Browse Source
- Add embedded Svelte admin web UI for process monitoring and control - Implement NIP-98 HTTP authentication middleware for secure API access - Add binary update/rollback system with versioned symlinks - Add admin API endpoints: status, config, binaries, update, restart, rollback - Update CI workflow to build all binaries for AMD64 and ARM64 architectures - Add launcher-web Makefile target for building admin UI separately Files modified: - .gitea/workflows/go.yml: Build all binaries and launcher admin UI - Makefile: Add launcher-web and orly-launcher-no-web targets - cmd/orly-launcher/auth.go: NIP-98 authentication middleware - cmd/orly-launcher/config.go: Admin UI configuration (port, owners) - cmd/orly-launcher/main.go: Start admin server, updated help text - cmd/orly-launcher/server.go: Admin HTTP server with API endpoints - cmd/orly-launcher/supervisor.go: GetProcessStatuses, RestartAll methods - cmd/orly-launcher/updater.go: Binary version management with symlinks - cmd/orly-launcher/web.go: Embedded admin UI serving - cmd/orly-launcher/web/: Svelte admin UI (dashboard, config, update pages) - pkg/version/version: Bump to v0.55.11 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>main v0.55.11
28 changed files with 3515 additions and 82 deletions
@ -0,0 +1,82 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/hex" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"git.mleku.dev/mleku/nostr/httpauth" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
) |
||||||
|
|
||||||
|
// AuthMiddleware provides NIP-98 authentication for the admin API.
|
||||||
|
type AuthMiddleware struct { |
||||||
|
owners map[string]struct{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewAuthMiddleware creates a new auth middleware with the given owner pubkeys.
|
||||||
|
func NewAuthMiddleware(owners []string) *AuthMiddleware { |
||||||
|
ownerMap := make(map[string]struct{}, len(owners)) |
||||||
|
for _, o := range owners { |
||||||
|
// Normalize to lowercase hex
|
||||||
|
ownerMap[strings.ToLower(o)] = struct{}{} |
||||||
|
} |
||||||
|
return &AuthMiddleware{owners: ownerMap} |
||||||
|
} |
||||||
|
|
||||||
|
// RequireAuth wraps a handler to require NIP-98 authentication from an owner.
|
||||||
|
func (a *AuthMiddleware) RequireAuth(next http.HandlerFunc) http.HandlerFunc { |
||||||
|
return func(w http.ResponseWriter, r *http.Request) { |
||||||
|
// Validate NIP-98 authentication
|
||||||
|
valid, pubkeyBytes, err := httpauth.CheckAuth(r) |
||||||
|
if chk.E(err) || !valid { |
||||||
|
errorMsg := "NIP-98 authentication required" |
||||||
|
if err != nil { |
||||||
|
errorMsg = err.Error() |
||||||
|
} |
||||||
|
http.Error(w, errorMsg, http.StatusUnauthorized) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Convert pubkey bytes to hex string
|
||||||
|
pubkeyHex := hex.EncodeToString(pubkeyBytes) |
||||||
|
|
||||||
|
// Check if pubkey is in owners list
|
||||||
|
if !a.IsOwner(pubkeyHex) { |
||||||
|
http.Error(w, "Not authorized - owner access required", http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Authentication successful, call the next handler
|
||||||
|
next(w, r) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// IsOwner checks if the given pubkey is in the owners list.
|
||||||
|
func (a *AuthMiddleware) IsOwner(pubkey string) bool { |
||||||
|
if len(a.owners) == 0 { |
||||||
|
// No owners configured - deny all access
|
||||||
|
return false |
||||||
|
} |
||||||
|
_, ok := a.owners[strings.ToLower(pubkey)] |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// AddOwner adds a pubkey to the owners list.
|
||||||
|
func (a *AuthMiddleware) AddOwner(pubkey string) { |
||||||
|
a.owners[strings.ToLower(pubkey)] = struct{}{} |
||||||
|
} |
||||||
|
|
||||||
|
// RemoveOwner removes a pubkey from the owners list.
|
||||||
|
func (a *AuthMiddleware) RemoveOwner(pubkey string) { |
||||||
|
delete(a.owners, strings.ToLower(pubkey)) |
||||||
|
} |
||||||
|
|
||||||
|
// Owners returns the list of owner pubkeys.
|
||||||
|
func (a *AuthMiddleware) Owners() []string { |
||||||
|
owners := make([]string, 0, len(a.owners)) |
||||||
|
for o := range a.owners { |
||||||
|
owners = append(owners, o) |
||||||
|
} |
||||||
|
return owners |
||||||
|
} |
||||||
@ -0,0 +1,316 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
) |
||||||
|
|
||||||
|
// AdminServer provides HTTP endpoints for managing the launcher.
|
||||||
|
type AdminServer struct { |
||||||
|
cfg *Config |
||||||
|
supervisor *Supervisor |
||||||
|
updater *Updater |
||||||
|
auth *AuthMiddleware |
||||||
|
server *http.Server |
||||||
|
startTime time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// NewAdminServer creates a new admin HTTP server.
|
||||||
|
func NewAdminServer(cfg *Config, supervisor *Supervisor) *AdminServer { |
||||||
|
return &AdminServer{ |
||||||
|
cfg: cfg, |
||||||
|
supervisor: supervisor, |
||||||
|
updater: NewUpdater(cfg.BinDir), |
||||||
|
auth: NewAuthMiddleware(cfg.AdminOwners), |
||||||
|
startTime: time.Now(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts the admin HTTP server.
|
||||||
|
func (s *AdminServer) Start(ctx context.Context) error { |
||||||
|
mux := http.NewServeMux() |
||||||
|
|
||||||
|
// Public endpoints
|
||||||
|
mux.HandleFunc("/admin", s.serveUI) |
||||||
|
mux.HandleFunc("/admin/", s.serveUI) |
||||||
|
|
||||||
|
// Authenticated API endpoints
|
||||||
|
mux.HandleFunc("/api/status", s.auth.RequireAuth(s.handleStatus)) |
||||||
|
mux.HandleFunc("/api/config", s.auth.RequireAuth(s.handleConfig)) |
||||||
|
mux.HandleFunc("/api/binaries", s.auth.RequireAuth(s.handleBinaries)) |
||||||
|
mux.HandleFunc("/api/update", s.auth.RequireAuth(s.handleUpdate)) |
||||||
|
mux.HandleFunc("/api/restart", s.auth.RequireAuth(s.handleRestart)) |
||||||
|
mux.HandleFunc("/api/rollback", s.auth.RequireAuth(s.handleRollback)) |
||||||
|
|
||||||
|
addr := fmt.Sprintf(":%d", s.cfg.AdminPort) |
||||||
|
s.server = &http.Server{ |
||||||
|
Addr: addr, |
||||||
|
Handler: mux, |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("starting admin server on %s", addr) |
||||||
|
|
||||||
|
go func() { |
||||||
|
<-ctx.Done() |
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
||||||
|
defer cancel() |
||||||
|
s.server.Shutdown(shutdownCtx) |
||||||
|
}() |
||||||
|
|
||||||
|
return s.server.ListenAndServe() |
||||||
|
} |
||||||
|
|
||||||
|
// StatusResponse is the response for GET /api/status
|
||||||
|
type StatusResponse struct { |
||||||
|
Version string `json:"version"` |
||||||
|
Uptime string `json:"uptime"` |
||||||
|
Processes []ProcessStatus `json:"processes"` |
||||||
|
} |
||||||
|
|
||||||
|
// ProcessStatus represents the status of a single managed process.
|
||||||
|
type ProcessStatus struct { |
||||||
|
Name string `json:"name"` |
||||||
|
Binary string `json:"binary"` |
||||||
|
Version string `json:"version"` |
||||||
|
Status string `json:"status"` |
||||||
|
PID int `json:"pid"` |
||||||
|
Restarts int `json:"restarts"` |
||||||
|
StartedAt string `json:"started_at,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleStatus(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != http.MethodGet { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
uptime := time.Since(s.startTime).Round(time.Second).String() |
||||||
|
processes := s.supervisor.GetProcessStatuses() |
||||||
|
|
||||||
|
response := StatusResponse{ |
||||||
|
Version: s.updater.CurrentVersion(), |
||||||
|
Uptime: uptime, |
||||||
|
Processes: processes, |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
} |
||||||
|
|
||||||
|
// ConfigResponse is the response for GET /api/config
|
||||||
|
type ConfigResponse struct { |
||||||
|
DBBackend string `json:"db_backend"` |
||||||
|
DBBinary string `json:"db_binary"` |
||||||
|
RelayBinary string `json:"relay_binary"` |
||||||
|
ACLBinary string `json:"acl_binary"` |
||||||
|
DBListen string `json:"db_listen"` |
||||||
|
ACLListen string `json:"acl_listen"` |
||||||
|
ACLEnabled bool `json:"acl_enabled"` |
||||||
|
ACLMode string `json:"acl_mode"` |
||||||
|
DataDir string `json:"data_dir"` |
||||||
|
LogLevel string `json:"log_level"` |
||||||
|
DistributedSyncEnabled bool `json:"distributed_sync_enabled"` |
||||||
|
ClusterSyncEnabled bool `json:"cluster_sync_enabled"` |
||||||
|
RelayGroupEnabled bool `json:"relay_group_enabled"` |
||||||
|
NegentropyEnabled bool `json:"negentropy_enabled"` |
||||||
|
AdminOwners []string `json:"admin_owners"` |
||||||
|
BinDir string `json:"bin_dir"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleConfig(w http.ResponseWriter, r *http.Request) { |
||||||
|
switch r.Method { |
||||||
|
case http.MethodGet: |
||||||
|
s.handleGetConfig(w, r) |
||||||
|
case http.MethodPost: |
||||||
|
s.handleSetConfig(w, r) |
||||||
|
default: |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleGetConfig(w http.ResponseWriter, r *http.Request) { |
||||||
|
response := ConfigResponse{ |
||||||
|
DBBackend: s.cfg.DBBackend, |
||||||
|
DBBinary: s.cfg.DBBinary, |
||||||
|
RelayBinary: s.cfg.RelayBinary, |
||||||
|
ACLBinary: s.cfg.ACLBinary, |
||||||
|
DBListen: s.cfg.DBListen, |
||||||
|
ACLListen: s.cfg.ACLListen, |
||||||
|
ACLEnabled: s.cfg.ACLEnabled, |
||||||
|
ACLMode: s.cfg.ACLMode, |
||||||
|
DataDir: s.cfg.DataDir, |
||||||
|
LogLevel: s.cfg.LogLevel, |
||||||
|
DistributedSyncEnabled: s.cfg.DistributedSyncEnabled, |
||||||
|
ClusterSyncEnabled: s.cfg.ClusterSyncEnabled, |
||||||
|
RelayGroupEnabled: s.cfg.RelayGroupEnabled, |
||||||
|
NegentropyEnabled: s.cfg.NegentropyEnabled, |
||||||
|
AdminOwners: s.auth.Owners(), |
||||||
|
BinDir: s.cfg.BinDir, |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleSetConfig(w http.ResponseWriter, r *http.Request) { |
||||||
|
// TODO: Implement config update (requires restart)
|
||||||
|
http.Error(w, "Config update not implemented yet", http.StatusNotImplemented) |
||||||
|
} |
||||||
|
|
||||||
|
// BinariesResponse is the response for GET /api/binaries
|
||||||
|
type BinariesResponse struct { |
||||||
|
CurrentVersion string `json:"current_version"` |
||||||
|
AvailableVersions []VersionInfo `json:"available_versions"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleBinaries(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != http.MethodGet { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
response := BinariesResponse{ |
||||||
|
CurrentVersion: s.updater.CurrentVersion(), |
||||||
|
AvailableVersions: s.updater.ListVersions(), |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
} |
||||||
|
|
||||||
|
// UpdateRequest is the request body for POST /api/update
|
||||||
|
type UpdateRequest struct { |
||||||
|
Version string `json:"version"` |
||||||
|
URLs map[string]string `json:"urls"` // binary name -> download URL
|
||||||
|
} |
||||||
|
|
||||||
|
// UpdateResponse is the response for POST /api/update
|
||||||
|
type UpdateResponse struct { |
||||||
|
Success bool `json:"success"` |
||||||
|
Message string `json:"message"` |
||||||
|
Version string `json:"version"` |
||||||
|
DownloadedFiles []string `json:"downloaded_files"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleUpdate(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var req UpdateRequest |
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); chk.E(err) { |
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if req.Version == "" { |
||||||
|
http.Error(w, "Version is required", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if len(req.URLs) == 0 { |
||||||
|
http.Error(w, "At least one binary URL is required", http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Perform the update
|
||||||
|
downloadedFiles, err := s.updater.Update(req.Version, req.URLs) |
||||||
|
if chk.E(err) { |
||||||
|
response := UpdateResponse{ |
||||||
|
Success: false, |
||||||
|
Message: err.Error(), |
||||||
|
Version: req.Version, |
||||||
|
} |
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
response := UpdateResponse{ |
||||||
|
Success: true, |
||||||
|
Message: fmt.Sprintf("Successfully updated to version %s", req.Version), |
||||||
|
Version: req.Version, |
||||||
|
DownloadedFiles: downloadedFiles, |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
} |
||||||
|
|
||||||
|
// RestartResponse is the response for POST /api/restart
|
||||||
|
type RestartResponse struct { |
||||||
|
Success bool `json:"success"` |
||||||
|
Message string `json:"message"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleRestart(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Signal supervisor to restart all processes
|
||||||
|
go func() { |
||||||
|
if err := s.supervisor.RestartAll(); chk.E(err) { |
||||||
|
log.E.F("restart failed: %v", err) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
response := RestartResponse{ |
||||||
|
Success: true, |
||||||
|
Message: "Restart initiated", |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
} |
||||||
|
|
||||||
|
// RollbackResponse is the response for POST /api/rollback
|
||||||
|
type RollbackResponse struct { |
||||||
|
Success bool `json:"success"` |
||||||
|
Message string `json:"message"` |
||||||
|
PreviousVersion string `json:"previous_version"` |
||||||
|
CurrentVersion string `json:"current_version"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) handleRollback(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != http.MethodPost { |
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
previousVersion := s.updater.CurrentVersion() |
||||||
|
|
||||||
|
if err := s.updater.Rollback(); chk.E(err) { |
||||||
|
response := RollbackResponse{ |
||||||
|
Success: false, |
||||||
|
Message: err.Error(), |
||||||
|
} |
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
response := RollbackResponse{ |
||||||
|
Success: true, |
||||||
|
Message: "Rollback successful - restart required to apply", |
||||||
|
PreviousVersion: previousVersion, |
||||||
|
CurrentVersion: s.updater.CurrentVersion(), |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json") |
||||||
|
json.NewEncoder(w).Encode(response) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *AdminServer) serveUI(w http.ResponseWriter, r *http.Request) { |
||||||
|
s.serveAdminUI(w, r) |
||||||
|
} |
||||||
@ -0,0 +1,287 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"path/filepath" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
) |
||||||
|
|
||||||
|
// Updater manages versioned binary updates with symlinks.
|
||||||
|
// Directory structure:
|
||||||
|
//
|
||||||
|
// ~/.local/share/orly/bin/
|
||||||
|
// versions/
|
||||||
|
// v0.55.10/
|
||||||
|
// orly
|
||||||
|
// orly-db-badger
|
||||||
|
// orly-acl-follows
|
||||||
|
// orly-launcher
|
||||||
|
// v0.55.11/
|
||||||
|
// ...
|
||||||
|
// current -> versions/v0.55.11 (symlink)
|
||||||
|
// orly -> current/orly (symlink)
|
||||||
|
// orly-db-badger -> current/orly-db-badger (symlink)
|
||||||
|
// ...
|
||||||
|
type Updater struct { |
||||||
|
binDir string // Base directory for binaries
|
||||||
|
versionsDir string // Directory containing version subdirectories
|
||||||
|
} |
||||||
|
|
||||||
|
// VersionInfo contains information about an installed version.
|
||||||
|
type VersionInfo struct { |
||||||
|
Version string `json:"version"` |
||||||
|
InstalledAt time.Time `json:"installed_at"` |
||||||
|
IsCurrent bool `json:"is_current"` |
||||||
|
Binaries []string `json:"binaries"` |
||||||
|
} |
||||||
|
|
||||||
|
// NewUpdater creates a new Updater.
|
||||||
|
func NewUpdater(binDir string) *Updater { |
||||||
|
return &Updater{ |
||||||
|
binDir: binDir, |
||||||
|
versionsDir: filepath.Join(binDir, "versions"), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// CurrentVersion returns the currently active version.
|
||||||
|
func (u *Updater) CurrentVersion() string { |
||||||
|
currentLink := filepath.Join(u.binDir, "current") |
||||||
|
target, err := os.Readlink(currentLink) |
||||||
|
if err != nil { |
||||||
|
return "unknown" |
||||||
|
} |
||||||
|
// Extract version from path like "versions/v0.55.10"
|
||||||
|
return filepath.Base(target) |
||||||
|
} |
||||||
|
|
||||||
|
// ListVersions returns all installed versions.
|
||||||
|
func (u *Updater) ListVersions() []VersionInfo { |
||||||
|
var versions []VersionInfo |
||||||
|
|
||||||
|
entries, err := os.ReadDir(u.versionsDir) |
||||||
|
if err != nil { |
||||||
|
return versions |
||||||
|
} |
||||||
|
|
||||||
|
currentVersion := u.CurrentVersion() |
||||||
|
|
||||||
|
for _, entry := range entries { |
||||||
|
if !entry.IsDir() { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
versionDir := filepath.Join(u.versionsDir, entry.Name()) |
||||||
|
info, err := entry.Info() |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// List binaries in this version
|
||||||
|
binaries, _ := u.listBinaries(versionDir) |
||||||
|
|
||||||
|
versions = append(versions, VersionInfo{ |
||||||
|
Version: entry.Name(), |
||||||
|
InstalledAt: info.ModTime(), |
||||||
|
IsCurrent: entry.Name() == currentVersion, |
||||||
|
Binaries: binaries, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Sort by version descending (newest first)
|
||||||
|
sort.Slice(versions, func(i, j int) bool { |
||||||
|
return versions[i].Version > versions[j].Version |
||||||
|
}) |
||||||
|
|
||||||
|
return versions |
||||||
|
} |
||||||
|
|
||||||
|
// listBinaries returns the list of binary files in a directory.
|
||||||
|
func (u *Updater) listBinaries(dir string) ([]string, error) { |
||||||
|
var binaries []string |
||||||
|
entries, err := os.ReadDir(dir) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
for _, entry := range entries { |
||||||
|
if entry.IsDir() { |
||||||
|
continue |
||||||
|
} |
||||||
|
info, err := entry.Info() |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
// Check if executable
|
||||||
|
if info.Mode()&0111 != 0 { |
||||||
|
binaries = append(binaries, entry.Name()) |
||||||
|
} |
||||||
|
} |
||||||
|
return binaries, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Update downloads binaries from URLs and installs them as a new version.
|
||||||
|
func (u *Updater) Update(version string, urls map[string]string) ([]string, error) { |
||||||
|
// Create version directory
|
||||||
|
versionDir := filepath.Join(u.versionsDir, version) |
||||||
|
if err := os.MkdirAll(versionDir, 0755); err != nil { |
||||||
|
return nil, fmt.Errorf("failed to create version directory: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
var downloadedFiles []string |
||||||
|
|
||||||
|
// Download each binary
|
||||||
|
for name, url := range urls { |
||||||
|
destPath := filepath.Join(versionDir, name) |
||||||
|
log.I.F("downloading %s from %s", name, url) |
||||||
|
|
||||||
|
if err := u.downloadFile(destPath, url); chk.E(err) { |
||||||
|
// Clean up on failure
|
||||||
|
os.RemoveAll(versionDir) |
||||||
|
return nil, fmt.Errorf("failed to download %s: %w", name, err) |
||||||
|
} |
||||||
|
|
||||||
|
// Make executable
|
||||||
|
if err := os.Chmod(destPath, 0755); err != nil { |
||||||
|
os.RemoveAll(versionDir) |
||||||
|
return nil, fmt.Errorf("failed to chmod %s: %w", name, err) |
||||||
|
} |
||||||
|
|
||||||
|
downloadedFiles = append(downloadedFiles, name) |
||||||
|
} |
||||||
|
|
||||||
|
// Update symlinks
|
||||||
|
if err := u.activateVersion(version); chk.E(err) { |
||||||
|
return downloadedFiles, fmt.Errorf("failed to activate version: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("successfully updated to version %s", version) |
||||||
|
return downloadedFiles, nil |
||||||
|
} |
||||||
|
|
||||||
|
// downloadFile downloads a file from a URL.
|
||||||
|
func (u *Updater) downloadFile(destPath, url string) error { |
||||||
|
resp, err := http.Get(url) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK { |
||||||
|
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) |
||||||
|
} |
||||||
|
|
||||||
|
out, err := os.Create(destPath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer out.Close() |
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// activateVersion updates symlinks to point to the specified version.
|
||||||
|
func (u *Updater) activateVersion(version string) error { |
||||||
|
versionDir := filepath.Join(u.versionsDir, version) |
||||||
|
|
||||||
|
// Verify version directory exists
|
||||||
|
if _, err := os.Stat(versionDir); os.IsNotExist(err) { |
||||||
|
return fmt.Errorf("version %s not found", version) |
||||||
|
} |
||||||
|
|
||||||
|
// Update 'current' symlink
|
||||||
|
currentLink := filepath.Join(u.binDir, "current") |
||||||
|
tempLink := currentLink + ".tmp" |
||||||
|
|
||||||
|
// Create new symlink to temp location
|
||||||
|
relPath, _ := filepath.Rel(u.binDir, versionDir) |
||||||
|
if err := os.Symlink(relPath, tempLink); err != nil { |
||||||
|
return fmt.Errorf("failed to create temp symlink: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Atomic rename
|
||||||
|
if err := os.Rename(tempLink, currentLink); err != nil { |
||||||
|
os.Remove(tempLink) |
||||||
|
return fmt.Errorf("failed to update current symlink: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Update individual binary symlinks
|
||||||
|
binaries, err := u.listBinaries(versionDir) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to list binaries: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
for _, binary := range binaries { |
||||||
|
binaryLink := filepath.Join(u.binDir, binary) |
||||||
|
tempBinaryLink := binaryLink + ".tmp" |
||||||
|
targetPath := filepath.Join("current", binary) |
||||||
|
|
||||||
|
// Create new symlink
|
||||||
|
if err := os.Symlink(targetPath, tempBinaryLink); err != nil { |
||||||
|
log.W.F("failed to create symlink for %s: %v", binary, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Atomic rename
|
||||||
|
if err := os.Rename(tempBinaryLink, binaryLink); err != nil { |
||||||
|
os.Remove(tempBinaryLink) |
||||||
|
log.W.F("failed to update symlink for %s: %v", binary, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Rollback reverts to the previous version.
|
||||||
|
func (u *Updater) Rollback() error { |
||||||
|
versions := u.ListVersions() |
||||||
|
if len(versions) < 2 { |
||||||
|
return fmt.Errorf("no previous version available for rollback") |
||||||
|
} |
||||||
|
|
||||||
|
// Find current version index
|
||||||
|
currentVersion := u.CurrentVersion() |
||||||
|
var previousVersion string |
||||||
|
|
||||||
|
for i, v := range versions { |
||||||
|
if v.Version == currentVersion && i+1 < len(versions) { |
||||||
|
previousVersion = versions[i+1].Version |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if previousVersion == "" { |
||||||
|
return fmt.Errorf("could not determine previous version") |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("rolling back from %s to %s", currentVersion, previousVersion) |
||||||
|
return u.activateVersion(previousVersion) |
||||||
|
} |
||||||
|
|
||||||
|
// GetBinaryVersion attempts to get the version from a binary using -v flag.
|
||||||
|
func (u *Updater) GetBinaryVersion(binaryPath string) string { |
||||||
|
cmd := exec.Command(binaryPath, "-v") |
||||||
|
output, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
// Try --version
|
||||||
|
cmd = exec.Command(binaryPath, "--version") |
||||||
|
output, err = cmd.Output() |
||||||
|
if err != nil { |
||||||
|
return "unknown" |
||||||
|
} |
||||||
|
} |
||||||
|
return strings.TrimSpace(string(output)) |
||||||
|
} |
||||||
|
|
||||||
|
// EnsureDirectories creates the required directory structure.
|
||||||
|
func (u *Updater) EnsureDirectories() error { |
||||||
|
return os.MkdirAll(u.versionsDir, 0755) |
||||||
|
} |
||||||
@ -0,0 +1,89 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"embed" |
||||||
|
"io/fs" |
||||||
|
"net/http" |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
//go:embed all:web/dist all:web/public
|
||||||
|
var adminFS embed.FS |
||||||
|
|
||||||
|
// getAdminFS returns the embedded filesystem for the admin UI.
|
||||||
|
func getAdminFS() (http.FileSystem, error) { |
||||||
|
// Try dist first (built assets)
|
||||||
|
distFS, err := fs.Sub(adminFS, "web/dist") |
||||||
|
if err == nil { |
||||||
|
// Check if dist has content
|
||||||
|
entries, _ := fs.ReadDir(distFS, ".") |
||||||
|
if len(entries) > 0 { |
||||||
|
return http.FS(distFS), nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Fall back to public (template)
|
||||||
|
publicFS, err := fs.Sub(adminFS, "web/public") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return http.FS(publicFS), nil |
||||||
|
} |
||||||
|
|
||||||
|
// serveAdminUI serves the embedded admin web UI.
|
||||||
|
func (s *AdminServer) serveAdminUI(w http.ResponseWriter, r *http.Request) { |
||||||
|
fsys, err := getAdminFS() |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Admin UI not available", http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Strip /admin prefix from path
|
||||||
|
urlPath := r.URL.Path |
||||||
|
if strings.HasPrefix(urlPath, "/admin") { |
||||||
|
urlPath = strings.TrimPrefix(urlPath, "/admin") |
||||||
|
if urlPath == "" { |
||||||
|
urlPath = "/" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Try to serve the file
|
||||||
|
filePath := strings.TrimPrefix(urlPath, "/") |
||||||
|
if filePath == "" { |
||||||
|
filePath = "index.html" |
||||||
|
} |
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
f, err := fsys.Open(filePath) |
||||||
|
if err != nil { |
||||||
|
// Serve index.html for SPA routing
|
||||||
|
filePath = "index.html" |
||||||
|
f, err = fsys.Open(filePath) |
||||||
|
if err != nil { |
||||||
|
http.Error(w, "Not found", http.StatusNotFound) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
f.Close() |
||||||
|
|
||||||
|
// Set content type
|
||||||
|
switch path.Ext(filePath) { |
||||||
|
case ".html": |
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
||||||
|
case ".css": |
||||||
|
w.Header().Set("Content-Type", "text/css; charset=utf-8") |
||||||
|
case ".js": |
||||||
|
w.Header().Set("Content-Type", "application/javascript; charset=utf-8") |
||||||
|
case ".json": |
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8") |
||||||
|
case ".svg": |
||||||
|
w.Header().Set("Content-Type", "image/svg+xml") |
||||||
|
case ".png": |
||||||
|
w.Header().Set("Content-Type", "image/png") |
||||||
|
} |
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
r.URL.Path = "/" + filePath |
||||||
|
http.FileServer(fsys).ServeHTTP(w, r) |
||||||
|
} |
||||||
@ -0,0 +1,272 @@ |
|||||||
|
{ |
||||||
|
"lockfileVersion": 1, |
||||||
|
"configVersion": 1, |
||||||
|
"workspaces": { |
||||||
|
"": { |
||||||
|
"name": "orly-launcher-admin", |
||||||
|
"dependencies": { |
||||||
|
"argon2-browser": "^1.18.0", |
||||||
|
"nostr-tools": "^2.1.4", |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@rollup/plugin-commonjs": "^25.0.7", |
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3", |
||||||
|
"@rollup/plugin-terser": "^0.4.4", |
||||||
|
"rollup": "^4.9.0", |
||||||
|
"rollup-plugin-css-only": "^4.5.2", |
||||||
|
"rollup-plugin-livereload": "^2.0.5", |
||||||
|
"rollup-plugin-svelte": "^7.1.6", |
||||||
|
"svelte": "^4.2.8", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
"packages": { |
||||||
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], |
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], |
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], |
||||||
|
|
||||||
|
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], |
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], |
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], |
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], |
||||||
|
|
||||||
|
"@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], |
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], |
||||||
|
|
||||||
|
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@25.0.8", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "glob": "^8.0.3", "is-reference": "1.2.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A=="], |
||||||
|
|
||||||
|
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="], |
||||||
|
|
||||||
|
"@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="], |
||||||
|
|
||||||
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], |
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], |
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], |
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], |
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], |
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], |
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], |
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], |
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], |
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], |
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], |
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], |
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], |
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], |
||||||
|
|
||||||
|
"@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="], |
||||||
|
|
||||||
|
"@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="], |
||||||
|
|
||||||
|
"@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="], |
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], |
||||||
|
|
||||||
|
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], |
||||||
|
|
||||||
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], |
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], |
||||||
|
|
||||||
|
"argon2-browser": ["argon2-browser@1.18.0", "", {}, "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw=="], |
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], |
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], |
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], |
||||||
|
|
||||||
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], |
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], |
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], |
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], |
||||||
|
|
||||||
|
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], |
||||||
|
|
||||||
|
"code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], |
||||||
|
|
||||||
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], |
||||||
|
|
||||||
|
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], |
||||||
|
|
||||||
|
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], |
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], |
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], |
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], |
||||||
|
|
||||||
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], |
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], |
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], |
||||||
|
|
||||||
|
"glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], |
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], |
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], |
||||||
|
|
||||||
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], |
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], |
||||||
|
|
||||||
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], |
||||||
|
|
||||||
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], |
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], |
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], |
||||||
|
|
||||||
|
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], |
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], |
||||||
|
|
||||||
|
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], |
||||||
|
|
||||||
|
"livereload": ["livereload@0.9.3", "", { "dependencies": { "chokidar": "^3.5.0", "livereload-js": "^3.3.1", "opts": ">= 1.2.0", "ws": "^7.4.3" }, "bin": { "livereload": "bin/livereload.js" } }, "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw=="], |
||||||
|
|
||||||
|
"livereload-js": ["livereload-js@3.4.1", "", {}, "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g=="], |
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], |
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
||||||
|
|
||||||
|
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], |
||||||
|
|
||||||
|
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], |
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], |
||||||
|
|
||||||
|
"nostr-tools": ["nostr-tools@2.20.0", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-Kq/2lMyeOdGvpDsYH2an8HP4H0aFCqwKythhTzxfgZTVv4L3NOgrJw2SxH8jkWlH8xPhWxGfN6lFtC+EAa2qYQ=="], |
||||||
|
|
||||||
|
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], |
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], |
||||||
|
|
||||||
|
"opts": ["opts@2.0.2", "", {}, "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg=="], |
||||||
|
|
||||||
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], |
||||||
|
|
||||||
|
"periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="], |
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], |
||||||
|
|
||||||
|
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], |
||||||
|
|
||||||
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], |
||||||
|
|
||||||
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], |
||||||
|
|
||||||
|
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], |
||||||
|
|
||||||
|
"rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], |
||||||
|
|
||||||
|
"rollup-plugin-css-only": ["rollup-plugin-css-only@4.5.5", "", { "dependencies": { "@rollup/pluginutils": "5" }, "peerDependencies": { "rollup": "<5" } }, "sha512-O2m2Sj8qsAtjUVqZyGTDXJypaOFFNV4knz8OlS6wJBws6XEICIiLsXmI56SbQEmWDqYU5TgRgWmslGj4THofJQ=="], |
||||||
|
|
||||||
|
"rollup-plugin-livereload": ["rollup-plugin-livereload@2.0.5", "", { "dependencies": { "livereload": "^0.9.1" } }, "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA=="], |
||||||
|
|
||||||
|
"rollup-plugin-svelte": ["rollup-plugin-svelte@7.2.3", "", { "dependencies": { "@rollup/pluginutils": "^4.1.0", "resolve.exports": "^2.0.0" }, "peerDependencies": { "rollup": ">=2.0.0", "svelte": ">=3.5.0" } }, "sha512-LlniP+h00DfM+E4eav/Kk8uGjgPUjGIBfrAS/IxQvsuFdqSM0Y2sXf31AdxuIGSW9GsmocDqOfaxR5QNno/Tgw=="], |
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], |
||||||
|
|
||||||
|
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], |
||||||
|
|
||||||
|
"smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="], |
||||||
|
|
||||||
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], |
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], |
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], |
||||||
|
|
||||||
|
"svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="], |
||||||
|
|
||||||
|
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], |
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], |
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], |
||||||
|
|
||||||
|
"ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], |
||||||
|
|
||||||
|
"@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], |
||||||
|
|
||||||
|
"@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], |
||||||
|
|
||||||
|
"@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], |
||||||
|
|
||||||
|
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], |
||||||
|
|
||||||
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
||||||
|
|
||||||
|
"code-red/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], |
||||||
|
|
||||||
|
"periscopic/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], |
||||||
|
|
||||||
|
"periscopic/is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], |
||||||
|
|
||||||
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
||||||
|
|
||||||
|
"rollup-plugin-svelte/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], |
||||||
|
|
||||||
|
"svelte/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], |
||||||
|
|
||||||
|
"svelte/is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], |
||||||
|
|
||||||
|
"@scure/bip32/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], |
||||||
|
|
||||||
|
"rollup-plugin-svelte/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
header.svelte-1bc06ax{background:var(--card-bg);border-bottom:1px solid var(--border-color);padding:0 20px}.header-content.svelte-1bc06ax{max-width:1200px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:60px}h1.svelte-1bc06ax{font-size:1.25rem;font-weight:600;color:var(--text-color)}nav.svelte-1bc06ax{display:flex;gap:4px}.nav-btn.svelte-1bc06ax{padding:8px 16px;background:none;border:none;border-radius:4px;color:var(--muted-color);cursor:pointer;font-size:0.9rem}.nav-btn.svelte-1bc06ax:hover{background:var(--border-color);color:var(--text-color)}.nav-btn.active.svelte-1bc06ax{background:var(--primary);color:white}.user-section.svelte-1bc06ax{display:flex;align-items:center;gap:12px}.pubkey.svelte-1bc06ax{font-family:monospace;font-size:0.85rem;color:var(--muted-color)}.logout-btn.svelte-1bc06ax,.login-header-btn.svelte-1bc06ax{padding:6px 14px;font-size:0.85rem;border-radius:4px;cursor:pointer}.logout-btn.svelte-1bc06ax{background:none;border:1px solid var(--border-color);color:var(--text-color)}.logout-btn.svelte-1bc06ax:hover{background:var(--border-color)}.login-header-btn.svelte-1bc06ax{background:var(--primary);border:none;color:white}.login-header-btn.svelte-1bc06ax:hover{background:var(--primary-hover)} |
||||||
|
.modal-overlay.svelte-rhbu32.svelte-rhbu32{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0, 0, 0, 0.5);display:flex;justify-content:center;align-items:center;z-index:1000}.modal.svelte-rhbu32.svelte-rhbu32{background:var(--card-bg, #fff);border-radius:8px;box-shadow:0 4px 20px rgba(0, 0, 0, 0.3);width:90%;max-width:450px;border:1px solid var(--border-color, #e0e0e0)}.modal-header.svelte-rhbu32.svelte-rhbu32{display:flex;justify-content:space-between;align-items:center;padding:20px;border-bottom:1px solid var(--border-color, #e0e0e0)}.modal-header.svelte-rhbu32 h2.svelte-rhbu32{margin:0;color:var(--text-color, #333);font-size:1.25rem}.close-btn.svelte-rhbu32.svelte-rhbu32{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--text-color, #333);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%}.close-btn.svelte-rhbu32.svelte-rhbu32:hover{background-color:var(--border-color, #e0e0e0)}.tab-container.svelte-rhbu32.svelte-rhbu32{padding:20px}.tabs.svelte-rhbu32.svelte-rhbu32{display:flex;border-bottom:1px solid var(--border-color, #e0e0e0);margin-bottom:20px}.tab-btn.svelte-rhbu32.svelte-rhbu32{flex:1;padding:12px 16px;background:none;border:none;cursor:pointer;color:var(--text-color, #333);font-size:1rem;border-bottom:2px solid transparent}.tab-btn.svelte-rhbu32.svelte-rhbu32:hover{background-color:var(--border-color, #e0e0e0)}.tab-btn.active.svelte-rhbu32.svelte-rhbu32{border-bottom-color:var(--primary, #00bcd4);color:var(--primary, #00bcd4)}.tab-content.svelte-rhbu32.svelte-rhbu32{min-height:180px}.extension-login.svelte-rhbu32.svelte-rhbu32,.nsec-login.svelte-rhbu32.svelte-rhbu32{display:flex;flex-direction:column;gap:16px}.extension-login.svelte-rhbu32 p.svelte-rhbu32,.nsec-login.svelte-rhbu32 p.svelte-rhbu32{margin:0;color:var(--muted-color, #666);line-height:1.5}.login-btn.svelte-rhbu32.svelte-rhbu32{padding:12px 24px;background:var(--primary, #00bcd4);color:white;border:none;border-radius:6px;cursor:pointer;font-size:1rem}.login-btn.svelte-rhbu32.svelte-rhbu32:hover:not(:disabled){background:var(--primary-hover, #00acc1)}.login-btn.svelte-rhbu32.svelte-rhbu32:disabled{background:#ccc;cursor:not-allowed}.nsec-input.svelte-rhbu32.svelte-rhbu32{padding:12px;border:1px solid var(--border-color, #e0e0e0);border-radius:6px;font-size:1rem;background:var(--card-bg, #fff);color:var(--text-color, #333)}.nsec-input.svelte-rhbu32.svelte-rhbu32:focus{outline:none;border-color:var(--primary, #00bcd4)}.generate-btn.svelte-rhbu32.svelte-rhbu32{padding:10px 20px;background:var(--success, #4caf50);color:white;border:none;border-radius:6px;cursor:pointer;font-size:0.95rem}.generate-btn.svelte-rhbu32.svelte-rhbu32:hover:not(:disabled){opacity:0.9}.generate-btn.svelte-rhbu32.svelte-rhbu32:disabled{background:#ccc;cursor:not-allowed}.generated-info.svelte-rhbu32.svelte-rhbu32{background:var(--bg-color, #f5f5f5);padding:12px;border-radius:6px;border:1px solid var(--border-color, #e0e0e0)}.generated-info.svelte-rhbu32 label.svelte-rhbu32{display:block;font-size:0.85rem;color:var(--muted-color, #666);margin-bottom:6px}.generated-info.svelte-rhbu32 code.svelte-rhbu32{display:block;word-break:break-all;font-size:0.8rem;color:var(--text-color, #333)}.message.svelte-rhbu32.svelte-rhbu32{padding:10px;border-radius:4px;margin-top:16px;text-align:center}.error-message.svelte-rhbu32.svelte-rhbu32{background:#ffebee;color:#c62828;border:1px solid #ffcdd2}.success-message.svelte-rhbu32.svelte-rhbu32{background:#e8f5e9;color:#2e7d32;border:1px solid #c8e6c9}.dark-theme.svelte-rhbu32 .error-message.svelte-rhbu32{background:#4a2c2a;color:#ffcdd2}.dark-theme.svelte-rhbu32 .success-message.svelte-rhbu32{background:#2e4a2e;color:#a5d6a7} |
||||||
|
.process-card.svelte-xh5u5u{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:16px}.process-header.svelte-xh5u5u{display:flex;align-items:center;gap:8px;margin-bottom:12px}.status-indicator.svelte-xh5u5u{font-size:1.2rem}.process-name.svelte-xh5u5u{font-weight:600;font-size:1rem;color:var(--text-color)}.process-details.svelte-xh5u5u{display:flex;flex-direction:column;gap:6px}.detail-row.svelte-xh5u5u{display:flex;justify-content:space-between;font-size:0.85rem}.label.svelte-xh5u5u{color:var(--muted-color)}.value.svelte-xh5u5u{color:var(--text-color);font-family:monospace}.value.binary.svelte-xh5u5u{font-size:0.75rem;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.value.warning.svelte-xh5u5u{color:var(--warning)} |
||||||
|
.dashboard.svelte-17dya06.svelte-17dya06{padding:20px 0}.page-header.svelte-17dya06.svelte-17dya06{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header.svelte-17dya06 h2.svelte-17dya06{font-size:1.5rem;color:var(--text-color)}.actions.svelte-17dya06.svelte-17dya06{display:flex;gap:8px}.refresh-btn.svelte-17dya06.svelte-17dya06,.restart-btn.svelte-17dya06.svelte-17dya06{padding:8px 16px;border-radius:4px;cursor:pointer;font-size:0.9rem}.refresh-btn.svelte-17dya06.svelte-17dya06{background:var(--card-bg);border:1px solid var(--border-color);color:var(--text-color)}.refresh-btn.svelte-17dya06.svelte-17dya06:hover:not(:disabled){background:var(--border-color)}.restart-btn.svelte-17dya06.svelte-17dya06{background:var(--warning);border:none;color:white}.restart-btn.svelte-17dya06.svelte-17dya06:hover:not(:disabled){opacity:0.9}.restart-btn.svelte-17dya06.svelte-17dya06:disabled,.refresh-btn.svelte-17dya06.svelte-17dya06:disabled{opacity:0.5;cursor:not-allowed}.error-banner.svelte-17dya06.svelte-17dya06{background:#ffebee;color:#c62828;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #ffcdd2}.status-summary.svelte-17dya06.svelte-17dya06{display:grid;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));gap:16px;margin-bottom:32px}.summary-card.svelte-17dya06.svelte-17dya06{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:4px}.summary-card.svelte-17dya06 .label.svelte-17dya06{font-size:0.85rem;color:var(--muted-color)}.summary-card.svelte-17dya06 .value.svelte-17dya06{font-size:1.25rem;font-weight:600;color:var(--text-color)}h3.svelte-17dya06.svelte-17dya06{font-size:1.1rem;color:var(--text-color);margin-bottom:16px}.processes-grid.svelte-17dya06.svelte-17dya06{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:16px}.loading.svelte-17dya06.svelte-17dya06{text-align:center;color:var(--muted-color);padding:40px} |
||||||
|
.config-page.svelte-1kruta9.svelte-1kruta9{padding:20px 0}.page-header.svelte-1kruta9.svelte-1kruta9{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header.svelte-1kruta9 h2.svelte-1kruta9{font-size:1.5rem;color:var(--text-color)}.refresh-btn.svelte-1kruta9.svelte-1kruta9{padding:8px 16px;background:var(--card-bg);border:1px solid var(--border-color);color:var(--text-color);border-radius:4px;cursor:pointer;font-size:0.9rem}.refresh-btn.svelte-1kruta9.svelte-1kruta9:hover:not(:disabled){background:var(--border-color)}.refresh-btn.svelte-1kruta9.svelte-1kruta9:disabled{opacity:0.5;cursor:not-allowed}.error-banner.svelte-1kruta9.svelte-1kruta9{background:#ffebee;color:#c62828;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #ffcdd2}.config-sections.svelte-1kruta9.svelte-1kruta9{display:flex;flex-direction:column;gap:24px}.config-section.svelte-1kruta9.svelte-1kruta9{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:20px}.config-section.svelte-1kruta9 h3.svelte-1kruta9{font-size:1.1rem;color:var(--text-color);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border-color)}.config-grid.svelte-1kruta9.svelte-1kruta9{display:grid;grid-template-columns:repeat(auto-fill, minmax(250px, 1fr));gap:16px}.config-item.svelte-1kruta9.svelte-1kruta9{display:flex;flex-direction:column;gap:4px}.config-item.full-width.svelte-1kruta9.svelte-1kruta9{grid-column:1 / -1}.config-item.svelte-1kruta9 .label.svelte-1kruta9{font-size:0.85rem;color:var(--muted-color)}.config-item.svelte-1kruta9 .value.svelte-1kruta9{font-size:0.95rem;color:var(--text-color)}.config-item.svelte-1kruta9 .value.mono.svelte-1kruta9{font-family:monospace;font-size:0.85rem}.config-item.svelte-1kruta9 .value.bool.svelte-1kruta9{font-weight:500}.config-item.svelte-1kruta9 .value.bool.enabled.svelte-1kruta9{color:var(--success)}.owners-list.svelte-1kruta9.svelte-1kruta9{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px}.owner.svelte-1kruta9.svelte-1kruta9{font-size:0.75rem;background:var(--bg-color);padding:4px 8px;border-radius:4px;word-break:break-all}.no-owners.svelte-1kruta9.svelte-1kruta9{color:var(--muted-color);font-style:italic}.config-note.svelte-1kruta9.svelte-1kruta9{margin-top:24px;padding:16px;background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px}.config-note.svelte-1kruta9 p.svelte-1kruta9{color:var(--muted-color);font-size:0.9rem;margin:0}.loading.svelte-1kruta9.svelte-1kruta9{text-align:center;color:var(--muted-color);padding:40px} |
||||||
|
.update-page.svelte-1ig49gt.svelte-1ig49gt{padding:20px 0}.page-header.svelte-1ig49gt.svelte-1ig49gt{margin-bottom:24px}.page-header.svelte-1ig49gt h2.svelte-1ig49gt{font-size:1.5rem;color:var(--text-color)}.error-banner.svelte-1ig49gt.svelte-1ig49gt{background:#ffebee;color:#c62828;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #ffcdd2}.success-banner.svelte-1ig49gt.svelte-1ig49gt{background:#e8f5e9;color:#2e7d32;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #c8e6c9}.current-version.svelte-1ig49gt.svelte-1ig49gt,.update-form.svelte-1ig49gt.svelte-1ig49gt,.versions-list.svelte-1ig49gt.svelte-1ig49gt{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:20px;margin-bottom:24px}h3.svelte-1ig49gt.svelte-1ig49gt{font-size:1.1rem;color:var(--text-color);margin-bottom:16px}.version-info.svelte-1ig49gt.svelte-1ig49gt{display:flex;align-items:center;justify-content:space-between}.version.svelte-1ig49gt.svelte-1ig49gt{font-size:1.5rem;font-weight:600;font-family:monospace;color:var(--text-color)}.rollback-btn.svelte-1ig49gt.svelte-1ig49gt{padding:8px 16px;background:var(--warning);border:none;color:white;border-radius:4px;cursor:pointer}.rollback-btn.svelte-1ig49gt.svelte-1ig49gt:hover:not(:disabled){opacity:0.9}.rollback-btn.svelte-1ig49gt.svelte-1ig49gt:disabled{opacity:0.5;cursor:not-allowed}.form-group.svelte-1ig49gt.svelte-1ig49gt{margin-bottom:20px}.form-group.svelte-1ig49gt>label.svelte-1ig49gt{display:block;font-size:0.9rem;color:var(--text-color);margin-bottom:8px;font-weight:500}.form-group.svelte-1ig49gt input[type="text"].svelte-1ig49gt{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:4px;font-size:0.95rem;background:var(--bg-color);color:var(--text-color)}.form-group.svelte-1ig49gt input.svelte-1ig49gt:focus{outline:none;border-color:var(--primary)}.url-header.svelte-1ig49gt.svelte-1ig49gt{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.url-header.svelte-1ig49gt label.svelte-1ig49gt{font-size:0.9rem;color:var(--text-color);font-weight:500}.helper-btn.svelte-1ig49gt.svelte-1ig49gt{padding:4px 12px;font-size:0.8rem;background:var(--card-bg);border:1px solid var(--border-color);border-radius:4px;color:var(--text-color);cursor:pointer}.helper-btn.svelte-1ig49gt.svelte-1ig49gt:hover:not(:disabled){background:var(--border-color)}.url-input.svelte-1ig49gt.svelte-1ig49gt{display:flex;gap:12px;align-items:center;margin-bottom:8px}.binary-name.svelte-1ig49gt.svelte-1ig49gt{width:140px;font-family:monospace;font-size:0.85rem;color:var(--muted-color)}.url-input.svelte-1ig49gt input.svelte-1ig49gt{flex:1;padding:8px 12px;border:1px solid var(--border-color);border-radius:4px;font-size:0.85rem;background:var(--bg-color);color:var(--text-color)}.update-btn.svelte-1ig49gt.svelte-1ig49gt{width:100%;padding:12px;background:var(--primary);border:none;color:white;border-radius:6px;font-size:1rem;cursor:pointer}.update-btn.svelte-1ig49gt.svelte-1ig49gt:hover:not(:disabled){background:var(--primary-hover)}.update-btn.svelte-1ig49gt.svelte-1ig49gt:disabled{opacity:0.5;cursor:not-allowed}table.svelte-1ig49gt.svelte-1ig49gt{width:100%;border-collapse:collapse}th.svelte-1ig49gt.svelte-1ig49gt,td.svelte-1ig49gt.svelte-1ig49gt{padding:10px 12px;text-align:left;border-bottom:1px solid var(--border-color)}th.svelte-1ig49gt.svelte-1ig49gt{font-size:0.85rem;color:var(--muted-color);font-weight:500}td.svelte-1ig49gt.svelte-1ig49gt{font-size:0.9rem;color:var(--text-color)}.version-cell.svelte-1ig49gt.svelte-1ig49gt{font-family:monospace}tr.current.svelte-1ig49gt.svelte-1ig49gt{background:rgba(0, 188, 212, 0.1)}.current-badge.svelte-1ig49gt.svelte-1ig49gt{background:var(--primary);color:white;padding:2px 8px;border-radius:4px;font-size:0.75rem} |
||||||
|
*{box-sizing:border-box;margin:0;padding:0}body{font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;background:var(--bg-color);color:var(--text-color);min-height:100vh}main.svelte-4k9oqz.svelte-4k9oqz{--bg-color:#f5f5f5;--card-bg:#ffffff;--text-color:#333333;--muted-color:#666666;--border-color:#e0e0e0;--primary:#00bcd4;--primary-hover:#00acc1;--success:#4caf50;--error:#f44336;--warning:#ff9800;min-height:100vh;background:var(--bg-color)}main.dark-theme.svelte-4k9oqz.svelte-4k9oqz{--bg-color:#1a1a1a;--card-bg:#2d2d2d;--text-color:#e0e0e0;--muted-color:#999999;--border-color:#444444}.content.svelte-4k9oqz.svelte-4k9oqz{max-width:1200px;margin:0 auto;padding:20px}.login-prompt.svelte-4k9oqz.svelte-4k9oqz{text-align:center;padding:60px 20px}.login-prompt.svelte-4k9oqz h2.svelte-4k9oqz{font-size:2rem;margin-bottom:16px;color:var(--text-color)}.login-prompt.svelte-4k9oqz p.svelte-4k9oqz{color:var(--muted-color);margin-bottom:24px}.login-btn.svelte-4k9oqz.svelte-4k9oqz{padding:12px 32px;font-size:1rem;background:var(--primary);color:white;border:none;border-radius:6px;cursor:pointer;transition:background 0.2s}.login-btn.svelte-4k9oqz.svelte-4k9oqz:hover{background:var(--primary-hover)} |
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,12 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"> |
||||||
|
<title>ORLY Launcher Admin</title> |
||||||
|
<link rel="stylesheet" href="/admin/bundle.css"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<script src="/admin/bundle.js"></script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
{ |
||||||
|
"name": "orly-launcher-admin", |
||||||
|
"version": "1.0.0", |
||||||
|
"private": true, |
||||||
|
"type": "module", |
||||||
|
"scripts": { |
||||||
|
"dev": "rollup -c -w", |
||||||
|
"build": "rollup -c" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@rollup/plugin-commonjs": "^25.0.7", |
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3", |
||||||
|
"@rollup/plugin-terser": "^0.4.4", |
||||||
|
"rollup": "^4.9.0", |
||||||
|
"rollup-plugin-css-only": "^4.5.2", |
||||||
|
"rollup-plugin-livereload": "^2.0.5", |
||||||
|
"rollup-plugin-svelte": "^7.1.6", |
||||||
|
"svelte": "^4.2.8" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"nostr-tools": "^2.1.4", |
||||||
|
"argon2-browser": "^1.18.0" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"> |
||||||
|
<title>ORLY Launcher Admin</title> |
||||||
|
<link rel="stylesheet" href="/admin/bundle.css"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<script src="/admin/bundle.js"></script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
import svelte from 'rollup-plugin-svelte'; |
||||||
|
import commonjs from '@rollup/plugin-commonjs'; |
||||||
|
import resolve from '@rollup/plugin-node-resolve'; |
||||||
|
import terser from '@rollup/plugin-terser'; |
||||||
|
import css from 'rollup-plugin-css-only'; |
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH; |
||||||
|
|
||||||
|
export default { |
||||||
|
input: 'src/main.js', |
||||||
|
output: { |
||||||
|
sourcemap: !production, |
||||||
|
format: 'iife', |
||||||
|
name: 'app', |
||||||
|
file: 'dist/bundle.js' |
||||||
|
}, |
||||||
|
plugins: [ |
||||||
|
svelte({ |
||||||
|
compilerOptions: { |
||||||
|
dev: !production |
||||||
|
} |
||||||
|
}), |
||||||
|
css({ output: 'bundle.css' }), |
||||||
|
resolve({ |
||||||
|
browser: true, |
||||||
|
dedupe: ['svelte'], |
||||||
|
exportConditions: ['svelte'] |
||||||
|
}), |
||||||
|
commonjs(), |
||||||
|
production && terser() |
||||||
|
], |
||||||
|
watch: { |
||||||
|
clearScreen: false |
||||||
|
} |
||||||
|
}; |
||||||
@ -0,0 +1,185 @@ |
|||||||
|
<script> |
||||||
|
import { onMount } from 'svelte'; |
||||||
|
import Header from './components/Header.svelte'; |
||||||
|
import LoginModal from './LoginModal.svelte'; |
||||||
|
import Dashboard from './pages/Dashboard.svelte'; |
||||||
|
import Config from './pages/Config.svelte'; |
||||||
|
import Update from './pages/Update.svelte'; |
||||||
|
import { isLoggedIn, userPubkey, userSigner, authMethod } from './stores.js'; |
||||||
|
|
||||||
|
let currentPage = 'dashboard'; |
||||||
|
let showLoginModal = false; |
||||||
|
let isDarkTheme = false; |
||||||
|
|
||||||
|
onMount(() => { |
||||||
|
// Check for stored auth |
||||||
|
const storedMethod = localStorage.getItem('launcher_auth_method'); |
||||||
|
const storedPubkey = localStorage.getItem('launcher_pubkey'); |
||||||
|
|
||||||
|
if (storedMethod === 'extension' && storedPubkey) { |
||||||
|
// Try to restore extension session |
||||||
|
if (window.nostr) { |
||||||
|
window.nostr.getPublicKey().then(pk => { |
||||||
|
if (pk === storedPubkey) { |
||||||
|
$isLoggedIn = true; |
||||||
|
$userPubkey = pk; |
||||||
|
$userSigner = window.nostr; |
||||||
|
$authMethod = 'extension'; |
||||||
|
} |
||||||
|
}).catch(() => { |
||||||
|
// Extension not available, clear stored auth |
||||||
|
localStorage.removeItem('launcher_auth_method'); |
||||||
|
localStorage.removeItem('launcher_pubkey'); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check for dark theme preference |
||||||
|
isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; |
||||||
|
}); |
||||||
|
|
||||||
|
function handleLogin(event) { |
||||||
|
const { method, pubkey, signer, privateKey } = event.detail; |
||||||
|
|
||||||
|
$isLoggedIn = true; |
||||||
|
$userPubkey = pubkey; |
||||||
|
$userSigner = signer; |
||||||
|
$authMethod = method; |
||||||
|
|
||||||
|
localStorage.setItem('launcher_auth_method', method); |
||||||
|
localStorage.setItem('launcher_pubkey', pubkey); |
||||||
|
|
||||||
|
if (method === 'nsec' && privateKey) { |
||||||
|
// Store encrypted key (handled by LoginModal) |
||||||
|
} |
||||||
|
|
||||||
|
showLoginModal = false; |
||||||
|
} |
||||||
|
|
||||||
|
function handleLogout() { |
||||||
|
$isLoggedIn = false; |
||||||
|
$userPubkey = ''; |
||||||
|
$userSigner = null; |
||||||
|
$authMethod = ''; |
||||||
|
|
||||||
|
localStorage.removeItem('launcher_auth_method'); |
||||||
|
localStorage.removeItem('launcher_pubkey'); |
||||||
|
localStorage.removeItem('launcher_privkey_encrypted'); |
||||||
|
} |
||||||
|
|
||||||
|
function navigateTo(page) { |
||||||
|
currentPage = page; |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<main class:dark-theme={isDarkTheme}> |
||||||
|
<Header |
||||||
|
{currentPage} |
||||||
|
isLoggedIn={$isLoggedIn} |
||||||
|
userPubkey={$userPubkey} |
||||||
|
on:navigate={(e) => navigateTo(e.detail)} |
||||||
|
on:login={() => showLoginModal = true} |
||||||
|
on:logout={handleLogout} |
||||||
|
/> |
||||||
|
|
||||||
|
<div class="content"> |
||||||
|
{#if !$isLoggedIn} |
||||||
|
<div class="login-prompt"> |
||||||
|
<h2>ORLY Launcher Admin</h2> |
||||||
|
<p>Please login to manage the relay services.</p> |
||||||
|
<button class="login-btn" on:click={() => showLoginModal = true}> |
||||||
|
Login with Nostr |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{:else if currentPage === 'dashboard'} |
||||||
|
<Dashboard /> |
||||||
|
{:else if currentPage === 'config'} |
||||||
|
<Config /> |
||||||
|
{:else if currentPage === 'update'} |
||||||
|
<Update /> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<LoginModal |
||||||
|
bind:showModal={showLoginModal} |
||||||
|
{isDarkTheme} |
||||||
|
on:login={handleLogin} |
||||||
|
on:close={() => showLoginModal = false} |
||||||
|
/> |
||||||
|
</main> |
||||||
|
|
||||||
|
<style> |
||||||
|
:global(*) { |
||||||
|
box-sizing: border-box; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
:global(body) { |
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
||||||
|
background: var(--bg-color); |
||||||
|
color: var(--text-color); |
||||||
|
min-height: 100vh; |
||||||
|
} |
||||||
|
|
||||||
|
main { |
||||||
|
--bg-color: #f5f5f5; |
||||||
|
--card-bg: #ffffff; |
||||||
|
--text-color: #333333; |
||||||
|
--muted-color: #666666; |
||||||
|
--border-color: #e0e0e0; |
||||||
|
--primary: #00bcd4; |
||||||
|
--primary-hover: #00acc1; |
||||||
|
--success: #4caf50; |
||||||
|
--error: #f44336; |
||||||
|
--warning: #ff9800; |
||||||
|
|
||||||
|
min-height: 100vh; |
||||||
|
background: var(--bg-color); |
||||||
|
} |
||||||
|
|
||||||
|
main.dark-theme { |
||||||
|
--bg-color: #1a1a1a; |
||||||
|
--card-bg: #2d2d2d; |
||||||
|
--text-color: #e0e0e0; |
||||||
|
--muted-color: #999999; |
||||||
|
--border-color: #444444; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
max-width: 1200px; |
||||||
|
margin: 0 auto; |
||||||
|
padding: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.login-prompt { |
||||||
|
text-align: center; |
||||||
|
padding: 60px 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.login-prompt h2 { |
||||||
|
font-size: 2rem; |
||||||
|
margin-bottom: 16px; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.login-prompt p { |
||||||
|
color: var(--muted-color); |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn { |
||||||
|
padding: 12px 32px; |
||||||
|
font-size: 1rem; |
||||||
|
background: var(--primary); |
||||||
|
color: white; |
||||||
|
border: none; |
||||||
|
border-radius: 6px; |
||||||
|
cursor: pointer; |
||||||
|
transition: background 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn:hover { |
||||||
|
background: var(--primary-hover); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,454 @@ |
|||||||
|
<script> |
||||||
|
import { createEventDispatcher, onMount } from 'svelte'; |
||||||
|
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'; |
||||||
|
import { nsecEncode, npubEncode, decode } from 'nostr-tools/nip19'; |
||||||
|
import { finalizeEvent } from 'nostr-tools/pure'; |
||||||
|
|
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
export let showModal = false; |
||||||
|
export let isDarkTheme = false; |
||||||
|
|
||||||
|
let activeTab = 'extension'; |
||||||
|
let nsecInput = ''; |
||||||
|
let isLoading = false; |
||||||
|
let errorMessage = ''; |
||||||
|
let successMessage = ''; |
||||||
|
let generatedNsec = ''; |
||||||
|
let generatedNpub = ''; |
||||||
|
|
||||||
|
function closeModal() { |
||||||
|
showModal = false; |
||||||
|
nsecInput = ''; |
||||||
|
errorMessage = ''; |
||||||
|
successMessage = ''; |
||||||
|
generatedNsec = ''; |
||||||
|
generatedNpub = ''; |
||||||
|
dispatch('close'); |
||||||
|
} |
||||||
|
|
||||||
|
function switchTab(tab) { |
||||||
|
activeTab = tab; |
||||||
|
errorMessage = ''; |
||||||
|
successMessage = ''; |
||||||
|
generatedNsec = ''; |
||||||
|
generatedNpub = ''; |
||||||
|
} |
||||||
|
|
||||||
|
async function generateNewKey() { |
||||||
|
errorMessage = ''; |
||||||
|
successMessage = ''; |
||||||
|
|
||||||
|
try { |
||||||
|
const secretKey = generateSecretKey(); |
||||||
|
const nsec = nsecEncode(secretKey); |
||||||
|
const pubkey = getPublicKey(secretKey); |
||||||
|
const npub = npubEncode(pubkey); |
||||||
|
|
||||||
|
generatedNsec = nsec; |
||||||
|
generatedNpub = npub; |
||||||
|
nsecInput = nsec; |
||||||
|
|
||||||
|
successMessage = 'New key generated!'; |
||||||
|
} catch (error) { |
||||||
|
errorMessage = 'Failed to generate key: ' + error.message; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function loginWithExtension() { |
||||||
|
isLoading = true; |
||||||
|
errorMessage = ''; |
||||||
|
successMessage = ''; |
||||||
|
|
||||||
|
try { |
||||||
|
if (!window.nostr) { |
||||||
|
throw new Error('No Nostr extension found. Please install nos2x or Alby.'); |
||||||
|
} |
||||||
|
|
||||||
|
const pubkey = await window.nostr.getPublicKey(); |
||||||
|
|
||||||
|
if (pubkey) { |
||||||
|
successMessage = 'Successfully logged in with extension!'; |
||||||
|
dispatch('login', { |
||||||
|
method: 'extension', |
||||||
|
pubkey: pubkey, |
||||||
|
signer: window.nostr, |
||||||
|
}); |
||||||
|
|
||||||
|
setTimeout(closeModal, 500); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
errorMessage = error.message; |
||||||
|
} finally { |
||||||
|
isLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function loginWithNsec() { |
||||||
|
isLoading = true; |
||||||
|
errorMessage = ''; |
||||||
|
successMessage = ''; |
||||||
|
|
||||||
|
try { |
||||||
|
if (!nsecInput.trim()) { |
||||||
|
throw new Error('Please enter your nsec'); |
||||||
|
} |
||||||
|
|
||||||
|
const trimmed = nsecInput.trim(); |
||||||
|
|
||||||
|
// Decode nsec |
||||||
|
let decoded; |
||||||
|
try { |
||||||
|
decoded = decode(trimmed); |
||||||
|
} catch { |
||||||
|
throw new Error('Invalid nsec format'); |
||||||
|
} |
||||||
|
|
||||||
|
if (decoded.type !== 'nsec') { |
||||||
|
throw new Error('Please enter an nsec (private key)'); |
||||||
|
} |
||||||
|
|
||||||
|
const secretKey = decoded.data; |
||||||
|
const publicKey = getPublicKey(secretKey); |
||||||
|
|
||||||
|
// Create a signer that uses the secret key |
||||||
|
const signer = { |
||||||
|
getPublicKey: async () => publicKey, |
||||||
|
signEvent: async (event) => { |
||||||
|
return finalizeEvent(event, secretKey); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
successMessage = 'Successfully logged in!'; |
||||||
|
dispatch('login', { |
||||||
|
method: 'nsec', |
||||||
|
pubkey: publicKey, |
||||||
|
privateKey: trimmed, |
||||||
|
signer: signer, |
||||||
|
}); |
||||||
|
|
||||||
|
setTimeout(closeModal, 500); |
||||||
|
} catch (error) { |
||||||
|
errorMessage = error.message; |
||||||
|
} finally { |
||||||
|
isLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function handleKeydown(event) { |
||||||
|
if (event.key === 'Escape') { |
||||||
|
closeModal(); |
||||||
|
} |
||||||
|
if (event.key === 'Enter' && activeTab === 'nsec') { |
||||||
|
loginWithNsec(); |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} /> |
||||||
|
|
||||||
|
{#if showModal} |
||||||
|
<div |
||||||
|
class="modal-overlay" |
||||||
|
on:click={closeModal} |
||||||
|
on:keydown={(e) => e.key === 'Escape' && closeModal()} |
||||||
|
role="button" |
||||||
|
tabindex="0" |
||||||
|
> |
||||||
|
<div |
||||||
|
class="modal" |
||||||
|
class:dark-theme={isDarkTheme} |
||||||
|
on:click|stopPropagation |
||||||
|
on:keydown|stopPropagation |
||||||
|
> |
||||||
|
<div class="modal-header"> |
||||||
|
<h2>Login to Launcher Admin</h2> |
||||||
|
<button class="close-btn" on:click={closeModal}>×</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="tab-container"> |
||||||
|
<div class="tabs"> |
||||||
|
<button |
||||||
|
class="tab-btn" |
||||||
|
class:active={activeTab === 'extension'} |
||||||
|
on:click={() => switchTab('extension')} |
||||||
|
> |
||||||
|
Extension |
||||||
|
</button> |
||||||
|
<button |
||||||
|
class="tab-btn" |
||||||
|
class:active={activeTab === 'nsec'} |
||||||
|
on:click={() => switchTab('nsec')} |
||||||
|
> |
||||||
|
Nsec |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="tab-content"> |
||||||
|
{#if activeTab === 'extension'} |
||||||
|
<div class="extension-login"> |
||||||
|
<p>Login using a NIP-07 browser extension like nos2x or Alby.</p> |
||||||
|
<button |
||||||
|
class="login-btn" |
||||||
|
on:click={loginWithExtension} |
||||||
|
disabled={isLoading} |
||||||
|
> |
||||||
|
{isLoading ? 'Connecting...' : 'Login with Extension'} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{:else} |
||||||
|
<div class="nsec-login"> |
||||||
|
<p>Enter your nsec or generate a new key pair.</p> |
||||||
|
|
||||||
|
<button |
||||||
|
class="generate-btn" |
||||||
|
on:click={generateNewKey} |
||||||
|
disabled={isLoading} |
||||||
|
> |
||||||
|
Generate New Key |
||||||
|
</button> |
||||||
|
|
||||||
|
{#if generatedNpub} |
||||||
|
<div class="generated-info"> |
||||||
|
<label>Your new public key (npub):</label> |
||||||
|
<code>{generatedNpub}</code> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<input |
||||||
|
type="password" |
||||||
|
placeholder="nsec1..." |
||||||
|
bind:value={nsecInput} |
||||||
|
disabled={isLoading} |
||||||
|
class="nsec-input" |
||||||
|
/> |
||||||
|
|
||||||
|
<button |
||||||
|
class="login-btn" |
||||||
|
on:click={loginWithNsec} |
||||||
|
disabled={isLoading || !nsecInput.trim()} |
||||||
|
> |
||||||
|
{isLoading ? 'Logging in...' : 'Login with Nsec'} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
{#if errorMessage} |
||||||
|
<div class="message error-message">{errorMessage}</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
{#if successMessage} |
||||||
|
<div class="message success-message">{successMessage}</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<style> |
||||||
|
.modal-overlay { |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background-color: rgba(0, 0, 0, 0.5); |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
z-index: 1000; |
||||||
|
} |
||||||
|
|
||||||
|
.modal { |
||||||
|
background: var(--card-bg, #fff); |
||||||
|
border-radius: 8px; |
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
||||||
|
width: 90%; |
||||||
|
max-width: 450px; |
||||||
|
border: 1px solid var(--border-color, #e0e0e0); |
||||||
|
} |
||||||
|
|
||||||
|
.modal-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
padding: 20px; |
||||||
|
border-bottom: 1px solid var(--border-color, #e0e0e0); |
||||||
|
} |
||||||
|
|
||||||
|
.modal-header h2 { |
||||||
|
margin: 0; |
||||||
|
color: var(--text-color, #333); |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
.close-btn { |
||||||
|
background: none; |
||||||
|
border: none; |
||||||
|
font-size: 1.5rem; |
||||||
|
cursor: pointer; |
||||||
|
color: var(--text-color, #333); |
||||||
|
padding: 0; |
||||||
|
width: 30px; |
||||||
|
height: 30px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
border-radius: 50%; |
||||||
|
} |
||||||
|
|
||||||
|
.close-btn:hover { |
||||||
|
background-color: var(--border-color, #e0e0e0); |
||||||
|
} |
||||||
|
|
||||||
|
.tab-container { |
||||||
|
padding: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.tabs { |
||||||
|
display: flex; |
||||||
|
border-bottom: 1px solid var(--border-color, #e0e0e0); |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.tab-btn { |
||||||
|
flex: 1; |
||||||
|
padding: 12px 16px; |
||||||
|
background: none; |
||||||
|
border: none; |
||||||
|
cursor: pointer; |
||||||
|
color: var(--text-color, #333); |
||||||
|
font-size: 1rem; |
||||||
|
border-bottom: 2px solid transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.tab-btn:hover { |
||||||
|
background-color: var(--border-color, #e0e0e0); |
||||||
|
} |
||||||
|
|
||||||
|
.tab-btn.active { |
||||||
|
border-bottom-color: var(--primary, #00bcd4); |
||||||
|
color: var(--primary, #00bcd4); |
||||||
|
} |
||||||
|
|
||||||
|
.tab-content { |
||||||
|
min-height: 180px; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-login, |
||||||
|
.nsec-login { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-login p, |
||||||
|
.nsec-login p { |
||||||
|
margin: 0; |
||||||
|
color: var(--muted-color, #666); |
||||||
|
line-height: 1.5; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn { |
||||||
|
padding: 12px 24px; |
||||||
|
background: var(--primary, #00bcd4); |
||||||
|
color: white; |
||||||
|
border: none; |
||||||
|
border-radius: 6px; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn:hover:not(:disabled) { |
||||||
|
background: var(--primary-hover, #00acc1); |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn:disabled { |
||||||
|
background: #ccc; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.nsec-input { |
||||||
|
padding: 12px; |
||||||
|
border: 1px solid var(--border-color, #e0e0e0); |
||||||
|
border-radius: 6px; |
||||||
|
font-size: 1rem; |
||||||
|
background: var(--card-bg, #fff); |
||||||
|
color: var(--text-color, #333); |
||||||
|
} |
||||||
|
|
||||||
|
.nsec-input:focus { |
||||||
|
outline: none; |
||||||
|
border-color: var(--primary, #00bcd4); |
||||||
|
} |
||||||
|
|
||||||
|
.generate-btn { |
||||||
|
padding: 10px 20px; |
||||||
|
background: var(--success, #4caf50); |
||||||
|
color: white; |
||||||
|
border: none; |
||||||
|
border-radius: 6px; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.95rem; |
||||||
|
} |
||||||
|
|
||||||
|
.generate-btn:hover:not(:disabled) { |
||||||
|
opacity: 0.9; |
||||||
|
} |
||||||
|
|
||||||
|
.generate-btn:disabled { |
||||||
|
background: #ccc; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.generated-info { |
||||||
|
background: var(--bg-color, #f5f5f5); |
||||||
|
padding: 12px; |
||||||
|
border-radius: 6px; |
||||||
|
border: 1px solid var(--border-color, #e0e0e0); |
||||||
|
} |
||||||
|
|
||||||
|
.generated-info label { |
||||||
|
display: block; |
||||||
|
font-size: 0.85rem; |
||||||
|
color: var(--muted-color, #666); |
||||||
|
margin-bottom: 6px; |
||||||
|
} |
||||||
|
|
||||||
|
.generated-info code { |
||||||
|
display: block; |
||||||
|
word-break: break-all; |
||||||
|
font-size: 0.8rem; |
||||||
|
color: var(--text-color, #333); |
||||||
|
} |
||||||
|
|
||||||
|
.message { |
||||||
|
padding: 10px; |
||||||
|
border-radius: 4px; |
||||||
|
margin-top: 16px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.error-message { |
||||||
|
background: #ffebee; |
||||||
|
color: #c62828; |
||||||
|
border: 1px solid #ffcdd2; |
||||||
|
} |
||||||
|
|
||||||
|
.success-message { |
||||||
|
background: #e8f5e9; |
||||||
|
color: #2e7d32; |
||||||
|
border: 1px solid #c8e6c9; |
||||||
|
} |
||||||
|
|
||||||
|
.dark-theme .error-message { |
||||||
|
background: #4a2c2a; |
||||||
|
color: #ffcdd2; |
||||||
|
} |
||||||
|
|
||||||
|
.dark-theme .success-message { |
||||||
|
background: #2e4a2e; |
||||||
|
color: #a5d6a7; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,151 @@ |
|||||||
|
/** |
||||||
|
* API helper functions for ORLY Launcher admin endpoints |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the API base URL (same as current page) |
||||||
|
*/ |
||||||
|
export function getApiBase() { |
||||||
|
return window.location.origin; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create NIP-98 authentication header |
||||||
|
* @param {object} signer - The signer instance |
||||||
|
* @param {string} pubkey - User's pubkey |
||||||
|
* @param {string} method - HTTP method |
||||||
|
* @param {string} url - Request URL |
||||||
|
* @returns {Promise<string|null>} Base64 encoded auth header or null |
||||||
|
*/ |
||||||
|
export async function createNIP98Auth(signer, pubkey, method, url) { |
||||||
|
if (!signer || !pubkey) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Create unsigned auth event
|
||||||
|
const authEvent = { |
||||||
|
kind: 27235, |
||||||
|
created_at: Math.floor(Date.now() / 1000), |
||||||
|
tags: [ |
||||||
|
["u", url], |
||||||
|
["method", method.toUpperCase()], |
||||||
|
], |
||||||
|
content: "", |
||||||
|
}; |
||||||
|
|
||||||
|
// Sign using the signer
|
||||||
|
const signedEvent = await signer.signEvent(authEvent); |
||||||
|
|
||||||
|
// Use URL-safe base64 encoding
|
||||||
|
const json = JSON.stringify(signedEvent); |
||||||
|
const base64 = btoa(json).replace(/\+/g, '-').replace(/\//g, '_'); |
||||||
|
return base64; |
||||||
|
} catch (error) { |
||||||
|
console.error("createNIP98Auth error:", error); |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Make an authenticated API request |
||||||
|
* @param {string} path - API path |
||||||
|
* @param {object} options - Fetch options |
||||||
|
* @param {object} signer - Signer instance |
||||||
|
* @param {string} pubkey - User's pubkey |
||||||
|
* @returns {Promise<Response>} |
||||||
|
*/ |
||||||
|
async function authFetch(path, options = {}, signer, pubkey) { |
||||||
|
const url = `${getApiBase()}${path}`; |
||||||
|
const method = options.method || 'GET'; |
||||||
|
const authHeader = await createNIP98Auth(signer, pubkey, method, url); |
||||||
|
|
||||||
|
const headers = { |
||||||
|
...options.headers, |
||||||
|
}; |
||||||
|
|
||||||
|
if (authHeader) { |
||||||
|
headers['Authorization'] = `Nostr ${authHeader}`; |
||||||
|
} |
||||||
|
|
||||||
|
return fetch(url, { ...options, headers }); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetch launcher status |
||||||
|
*/ |
||||||
|
export async function fetchStatus(signer, pubkey) { |
||||||
|
const response = await authFetch('/api/status', {}, signer, pubkey); |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Failed to fetch status: ${response.statusText}`); |
||||||
|
} |
||||||
|
return response.json(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetch launcher configuration |
||||||
|
*/ |
||||||
|
export async function fetchConfig(signer, pubkey) { |
||||||
|
const response = await authFetch('/api/config', {}, signer, pubkey); |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Failed to fetch config: ${response.statusText}`); |
||||||
|
} |
||||||
|
return response.json(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetch available binaries |
||||||
|
*/ |
||||||
|
export async function fetchBinaries(signer, pubkey) { |
||||||
|
const response = await authFetch('/api/binaries', {}, signer, pubkey); |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Failed to fetch binaries: ${response.statusText}`); |
||||||
|
} |
||||||
|
return response.json(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Update binaries from URLs |
||||||
|
*/ |
||||||
|
export async function updateBinaries(signer, pubkey, version, urls) { |
||||||
|
const response = await authFetch('/api/update', { |
||||||
|
method: 'POST', |
||||||
|
headers: { 'Content-Type': 'application/json' }, |
||||||
|
body: JSON.stringify({ version, urls }), |
||||||
|
}, signer, pubkey); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const data = await response.json(); |
||||||
|
throw new Error(data.message || `Update failed: ${response.statusText}`); |
||||||
|
} |
||||||
|
return response.json(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Restart all services |
||||||
|
*/ |
||||||
|
export async function restartServices(signer, pubkey) { |
||||||
|
const response = await authFetch('/api/restart', { |
||||||
|
method: 'POST', |
||||||
|
}, signer, pubkey); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Restart failed: ${response.statusText}`); |
||||||
|
} |
||||||
|
return response.json(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Rollback to previous version |
||||||
|
*/ |
||||||
|
export async function rollbackVersion(signer, pubkey) { |
||||||
|
const response = await authFetch('/api/rollback', { |
||||||
|
method: 'POST', |
||||||
|
}, signer, pubkey); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const data = await response.json(); |
||||||
|
throw new Error(data.message || `Rollback failed: ${response.statusText}`); |
||||||
|
} |
||||||
|
return response.json(); |
||||||
|
} |
||||||
@ -0,0 +1,149 @@ |
|||||||
|
<script> |
||||||
|
import { createEventDispatcher } from 'svelte'; |
||||||
|
|
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
export let currentPage = 'dashboard'; |
||||||
|
export let isLoggedIn = false; |
||||||
|
export let userPubkey = ''; |
||||||
|
|
||||||
|
function navigate(page) { |
||||||
|
dispatch('navigate', page); |
||||||
|
} |
||||||
|
|
||||||
|
function formatPubkey(pk) { |
||||||
|
if (!pk) return ''; |
||||||
|
return pk.slice(0, 8) + '...' + pk.slice(-4); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<header> |
||||||
|
<div class="header-content"> |
||||||
|
<h1>ORLY Launcher</h1> |
||||||
|
|
||||||
|
{#if isLoggedIn} |
||||||
|
<nav> |
||||||
|
<button |
||||||
|
class="nav-btn" |
||||||
|
class:active={currentPage === 'dashboard'} |
||||||
|
on:click={() => navigate('dashboard')} |
||||||
|
> |
||||||
|
Dashboard |
||||||
|
</button> |
||||||
|
<button |
||||||
|
class="nav-btn" |
||||||
|
class:active={currentPage === 'config'} |
||||||
|
on:click={() => navigate('config')} |
||||||
|
> |
||||||
|
Config |
||||||
|
</button> |
||||||
|
<button |
||||||
|
class="nav-btn" |
||||||
|
class:active={currentPage === 'update'} |
||||||
|
on:click={() => navigate('update')} |
||||||
|
> |
||||||
|
Update |
||||||
|
</button> |
||||||
|
</nav> |
||||||
|
|
||||||
|
<div class="user-section"> |
||||||
|
<span class="pubkey">{formatPubkey(userPubkey)}</span> |
||||||
|
<button class="logout-btn" on:click={() => dispatch('logout')}> |
||||||
|
Logout |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{:else} |
||||||
|
<button class="login-header-btn" on:click={() => dispatch('login')}> |
||||||
|
Login |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
|
||||||
|
<style> |
||||||
|
header { |
||||||
|
background: var(--card-bg); |
||||||
|
border-bottom: 1px solid var(--border-color); |
||||||
|
padding: 0 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.header-content { |
||||||
|
max-width: 1200px; |
||||||
|
margin: 0 auto; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
height: 60px; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: 1.25rem; |
||||||
|
font-weight: 600; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
nav { |
||||||
|
display: flex; |
||||||
|
gap: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.nav-btn { |
||||||
|
padding: 8px 16px; |
||||||
|
background: none; |
||||||
|
border: none; |
||||||
|
border-radius: 4px; |
||||||
|
color: var(--muted-color); |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.9rem; |
||||||
|
} |
||||||
|
|
||||||
|
.nav-btn:hover { |
||||||
|
background: var(--border-color); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.nav-btn.active { |
||||||
|
background: var(--primary); |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.user-section { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
.pubkey { |
||||||
|
font-family: monospace; |
||||||
|
font-size: 0.85rem; |
||||||
|
color: var(--muted-color); |
||||||
|
} |
||||||
|
|
||||||
|
.logout-btn, |
||||||
|
.login-header-btn { |
||||||
|
padding: 6px 14px; |
||||||
|
font-size: 0.85rem; |
||||||
|
border-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.logout-btn { |
||||||
|
background: none; |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.logout-btn:hover { |
||||||
|
background: var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
.login-header-btn { |
||||||
|
background: var(--primary); |
||||||
|
border: none; |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.login-header-btn:hover { |
||||||
|
background: var(--primary-hover); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
<script> |
||||||
|
export let process; |
||||||
|
|
||||||
|
function getStatusColor(status) { |
||||||
|
switch (status) { |
||||||
|
case 'running': return 'var(--success)'; |
||||||
|
case 'stopped': return 'var(--muted-color)'; |
||||||
|
case 'crashed': return 'var(--error)'; |
||||||
|
default: return 'var(--muted-color)'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getStatusIcon(status) { |
||||||
|
switch (status) { |
||||||
|
case 'running': return '●'; |
||||||
|
case 'stopped': return '○'; |
||||||
|
case 'crashed': return '✗'; |
||||||
|
default: return '?'; |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="process-card"> |
||||||
|
<div class="process-header"> |
||||||
|
<span class="status-indicator" style="color: {getStatusColor(process.status)}"> |
||||||
|
{getStatusIcon(process.status)} |
||||||
|
</span> |
||||||
|
<span class="process-name">{process.name}</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="process-details"> |
||||||
|
<div class="detail-row"> |
||||||
|
<span class="label">Status:</span> |
||||||
|
<span class="value" style="color: {getStatusColor(process.status)}"> |
||||||
|
{process.status} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if process.pid > 0} |
||||||
|
<div class="detail-row"> |
||||||
|
<span class="label">PID:</span> |
||||||
|
<span class="value">{process.pid}</span> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<div class="detail-row"> |
||||||
|
<span class="label">Binary:</span> |
||||||
|
<span class="value binary">{process.binary}</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if process.restarts > 0} |
||||||
|
<div class="detail-row"> |
||||||
|
<span class="label">Restarts:</span> |
||||||
|
<span class="value warning">{process.restarts}</span> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.process-card { |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.process-header { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 8px; |
||||||
|
margin-bottom: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
.status-indicator { |
||||||
|
font-size: 1.2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.process-name { |
||||||
|
font-weight: 600; |
||||||
|
font-size: 1rem; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.process-details { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 6px; |
||||||
|
} |
||||||
|
|
||||||
|
.detail-row { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 0.85rem; |
||||||
|
} |
||||||
|
|
||||||
|
.label { |
||||||
|
color: var(--muted-color); |
||||||
|
} |
||||||
|
|
||||||
|
.value { |
||||||
|
color: var(--text-color); |
||||||
|
font-family: monospace; |
||||||
|
} |
||||||
|
|
||||||
|
.value.binary { |
||||||
|
font-size: 0.75rem; |
||||||
|
max-width: 150px; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
|
||||||
|
.value.warning { |
||||||
|
color: var(--warning); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
import App from './App.svelte'; |
||||||
|
|
||||||
|
const app = new App({ |
||||||
|
target: document.body, |
||||||
|
}); |
||||||
|
|
||||||
|
export default app; |
||||||
@ -0,0 +1,300 @@ |
|||||||
|
<script> |
||||||
|
import { onMount } from 'svelte'; |
||||||
|
import { userSigner, userPubkey, configData, isLoading, error } from '../stores.js'; |
||||||
|
import { fetchConfig } from '../api.js'; |
||||||
|
|
||||||
|
onMount(async () => { |
||||||
|
await loadConfig(); |
||||||
|
}); |
||||||
|
|
||||||
|
async function loadConfig() { |
||||||
|
$isLoading = true; |
||||||
|
try { |
||||||
|
$configData = await fetchConfig($userSigner, $userPubkey); |
||||||
|
$error = ''; |
||||||
|
} catch (e) { |
||||||
|
$error = e.message; |
||||||
|
} finally { |
||||||
|
$isLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="config-page"> |
||||||
|
<div class="page-header"> |
||||||
|
<h2>Configuration</h2> |
||||||
|
<button class="refresh-btn" on:click={loadConfig} disabled={$isLoading}> |
||||||
|
Refresh |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if $error} |
||||||
|
<div class="error-banner">{$error}</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
{#if $configData} |
||||||
|
<div class="config-sections"> |
||||||
|
<section class="config-section"> |
||||||
|
<h3>Database</h3> |
||||||
|
<div class="config-grid"> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Backend</span> |
||||||
|
<span class="value">{$configData.db_backend}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Binary</span> |
||||||
|
<span class="value mono">{$configData.db_binary}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Listen Address</span> |
||||||
|
<span class="value mono">{$configData.db_listen}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Data Directory</span> |
||||||
|
<span class="value mono">{$configData.data_dir}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="config-section"> |
||||||
|
<h3>ACL</h3> |
||||||
|
<div class="config-grid"> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Enabled</span> |
||||||
|
<span class="value bool" class:enabled={$configData.acl_enabled}> |
||||||
|
{$configData.acl_enabled ? 'Yes' : 'No'} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Mode</span> |
||||||
|
<span class="value">{$configData.acl_mode}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Binary</span> |
||||||
|
<span class="value mono">{$configData.acl_binary}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Listen Address</span> |
||||||
|
<span class="value mono">{$configData.acl_listen}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="config-section"> |
||||||
|
<h3>Relay</h3> |
||||||
|
<div class="config-grid"> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Binary</span> |
||||||
|
<span class="value mono">{$configData.relay_binary}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Log Level</span> |
||||||
|
<span class="value">{$configData.log_level}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="config-section"> |
||||||
|
<h3>Sync Services</h3> |
||||||
|
<div class="config-grid"> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Distributed Sync</span> |
||||||
|
<span class="value bool" class:enabled={$configData.distributed_sync_enabled}> |
||||||
|
{$configData.distributed_sync_enabled ? 'Enabled' : 'Disabled'} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Cluster Sync</span> |
||||||
|
<span class="value bool" class:enabled={$configData.cluster_sync_enabled}> |
||||||
|
{$configData.cluster_sync_enabled ? 'Enabled' : 'Disabled'} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Relay Group</span> |
||||||
|
<span class="value bool" class:enabled={$configData.relay_group_enabled}> |
||||||
|
{$configData.relay_group_enabled ? 'Enabled' : 'Disabled'} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Negentropy</span> |
||||||
|
<span class="value bool" class:enabled={$configData.negentropy_enabled}> |
||||||
|
{$configData.negentropy_enabled ? 'Enabled' : 'Disabled'} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="config-section"> |
||||||
|
<h3>Admin</h3> |
||||||
|
<div class="config-grid"> |
||||||
|
<div class="config-item"> |
||||||
|
<span class="label">Binary Directory</span> |
||||||
|
<span class="value mono">{$configData.bin_dir}</span> |
||||||
|
</div> |
||||||
|
<div class="config-item full-width"> |
||||||
|
<span class="label">Admin Owners</span> |
||||||
|
<div class="owners-list"> |
||||||
|
{#each $configData.admin_owners || [] as owner} |
||||||
|
<code class="owner">{owner}</code> |
||||||
|
{:else} |
||||||
|
<span class="no-owners">No owners configured</span> |
||||||
|
{/each} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="config-note"> |
||||||
|
<p>Configuration is loaded from environment variables. To change settings, update the environment and restart the launcher.</p> |
||||||
|
</div> |
||||||
|
{:else if !$error} |
||||||
|
<div class="loading">Loading configuration...</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.config-page { |
||||||
|
padding: 20px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header h2 { |
||||||
|
font-size: 1.5rem; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.refresh-btn { |
||||||
|
padding: 8px 16px; |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
color: var(--text-color); |
||||||
|
border-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.9rem; |
||||||
|
} |
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) { |
||||||
|
background: var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
.refresh-btn:disabled { |
||||||
|
opacity: 0.5; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.error-banner { |
||||||
|
background: #ffebee; |
||||||
|
color: #c62828; |
||||||
|
padding: 12px 16px; |
||||||
|
border-radius: 6px; |
||||||
|
margin-bottom: 20px; |
||||||
|
border: 1px solid #ffcdd2; |
||||||
|
} |
||||||
|
|
||||||
|
.config-sections { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.config-section { |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.config-section h3 { |
||||||
|
font-size: 1.1rem; |
||||||
|
color: var(--text-color); |
||||||
|
margin-bottom: 16px; |
||||||
|
padding-bottom: 8px; |
||||||
|
border-bottom: 1px solid var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
.config-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
||||||
|
gap: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.config-item { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.config-item.full-width { |
||||||
|
grid-column: 1 / -1; |
||||||
|
} |
||||||
|
|
||||||
|
.config-item .label { |
||||||
|
font-size: 0.85rem; |
||||||
|
color: var(--muted-color); |
||||||
|
} |
||||||
|
|
||||||
|
.config-item .value { |
||||||
|
font-size: 0.95rem; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.config-item .value.mono { |
||||||
|
font-family: monospace; |
||||||
|
font-size: 0.85rem; |
||||||
|
} |
||||||
|
|
||||||
|
.config-item .value.bool { |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.config-item .value.bool.enabled { |
||||||
|
color: var(--success); |
||||||
|
} |
||||||
|
|
||||||
|
.owners-list { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
gap: 8px; |
||||||
|
margin-top: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.owner { |
||||||
|
font-size: 0.75rem; |
||||||
|
background: var(--bg-color); |
||||||
|
padding: 4px 8px; |
||||||
|
border-radius: 4px; |
||||||
|
word-break: break-all; |
||||||
|
} |
||||||
|
|
||||||
|
.no-owners { |
||||||
|
color: var(--muted-color); |
||||||
|
font-style: italic; |
||||||
|
} |
||||||
|
|
||||||
|
.config-note { |
||||||
|
margin-top: 24px; |
||||||
|
padding: 16px; |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
.config-note p { |
||||||
|
color: var(--muted-color); |
||||||
|
font-size: 0.9rem; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.loading { |
||||||
|
text-align: center; |
||||||
|
color: var(--muted-color); |
||||||
|
padding: 40px; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,202 @@ |
|||||||
|
<script> |
||||||
|
import { onMount, onDestroy } from 'svelte'; |
||||||
|
import { userSigner, userPubkey, statusData, isLoading, error } from '../stores.js'; |
||||||
|
import { fetchStatus, restartServices } from '../api.js'; |
||||||
|
import ProcessCard from '../components/ProcessCard.svelte'; |
||||||
|
|
||||||
|
let refreshInterval; |
||||||
|
|
||||||
|
onMount(async () => { |
||||||
|
await loadStatus(); |
||||||
|
// Auto-refresh every 5 seconds |
||||||
|
refreshInterval = setInterval(loadStatus, 5000); |
||||||
|
}); |
||||||
|
|
||||||
|
onDestroy(() => { |
||||||
|
if (refreshInterval) { |
||||||
|
clearInterval(refreshInterval); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
async function loadStatus() { |
||||||
|
try { |
||||||
|
$statusData = await fetchStatus($userSigner, $userPubkey); |
||||||
|
$error = ''; |
||||||
|
} catch (e) { |
||||||
|
$error = e.message; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function handleRestart() { |
||||||
|
if (!confirm('Are you sure you want to restart all services?')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$isLoading = true; |
||||||
|
try { |
||||||
|
await restartServices($userSigner, $userPubkey); |
||||||
|
// Wait a moment then refresh |
||||||
|
setTimeout(loadStatus, 2000); |
||||||
|
} catch (e) { |
||||||
|
$error = e.message; |
||||||
|
} finally { |
||||||
|
$isLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="dashboard"> |
||||||
|
<div class="page-header"> |
||||||
|
<h2>Dashboard</h2> |
||||||
|
<div class="actions"> |
||||||
|
<button class="refresh-btn" on:click={loadStatus} disabled={$isLoading}> |
||||||
|
Refresh |
||||||
|
</button> |
||||||
|
<button class="restart-btn" on:click={handleRestart} disabled={$isLoading}> |
||||||
|
Restart All |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if $error} |
||||||
|
<div class="error-banner">{$error}</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
{#if $statusData} |
||||||
|
<div class="status-summary"> |
||||||
|
<div class="summary-card"> |
||||||
|
<span class="label">Version</span> |
||||||
|
<span class="value">{$statusData.version}</span> |
||||||
|
</div> |
||||||
|
<div class="summary-card"> |
||||||
|
<span class="label">Uptime</span> |
||||||
|
<span class="value">{$statusData.uptime}</span> |
||||||
|
</div> |
||||||
|
<div class="summary-card"> |
||||||
|
<span class="label">Processes</span> |
||||||
|
<span class="value">{$statusData.processes?.length || 0}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h3>Managed Processes</h3> |
||||||
|
<div class="processes-grid"> |
||||||
|
{#each $statusData.processes || [] as process} |
||||||
|
<ProcessCard {process} /> |
||||||
|
{/each} |
||||||
|
</div> |
||||||
|
{:else if !$error} |
||||||
|
<div class="loading">Loading status...</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.dashboard { |
||||||
|
padding: 20px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header h2 { |
||||||
|
font-size: 1.5rem; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.actions { |
||||||
|
display: flex; |
||||||
|
gap: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
.refresh-btn, |
||||||
|
.restart-btn { |
||||||
|
padding: 8px 16px; |
||||||
|
border-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.9rem; |
||||||
|
} |
||||||
|
|
||||||
|
.refresh-btn { |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) { |
||||||
|
background: var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
.restart-btn { |
||||||
|
background: var(--warning); |
||||||
|
border: none; |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.restart-btn:hover:not(:disabled) { |
||||||
|
opacity: 0.9; |
||||||
|
} |
||||||
|
|
||||||
|
.restart-btn:disabled, |
||||||
|
.refresh-btn:disabled { |
||||||
|
opacity: 0.5; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.error-banner { |
||||||
|
background: #ffebee; |
||||||
|
color: #c62828; |
||||||
|
padding: 12px 16px; |
||||||
|
border-radius: 6px; |
||||||
|
margin-bottom: 20px; |
||||||
|
border: 1px solid #ffcdd2; |
||||||
|
} |
||||||
|
|
||||||
|
.status-summary { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
||||||
|
gap: 16px; |
||||||
|
margin-bottom: 32px; |
||||||
|
} |
||||||
|
|
||||||
|
.summary-card { |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 16px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.summary-card .label { |
||||||
|
font-size: 0.85rem; |
||||||
|
color: var(--muted-color); |
||||||
|
} |
||||||
|
|
||||||
|
.summary-card .value { |
||||||
|
font-size: 1.25rem; |
||||||
|
font-weight: 600; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: 1.1rem; |
||||||
|
color: var(--text-color); |
||||||
|
margin-bottom: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.processes-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
||||||
|
gap: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.loading { |
||||||
|
text-align: center; |
||||||
|
color: var(--muted-color); |
||||||
|
padding: 40px; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,430 @@ |
|||||||
|
<script> |
||||||
|
import { onMount } from 'svelte'; |
||||||
|
import { userSigner, userPubkey, binariesData, isLoading, error } from '../stores.js'; |
||||||
|
import { fetchBinaries, updateBinaries, rollbackVersion } from '../api.js'; |
||||||
|
|
||||||
|
let version = ''; |
||||||
|
let urls = { |
||||||
|
'orly': '', |
||||||
|
'orly-db-badger': '', |
||||||
|
'orly-acl-follows': '', |
||||||
|
'orly-launcher': '', |
||||||
|
}; |
||||||
|
let updateResult = null; |
||||||
|
let isUpdating = false; |
||||||
|
|
||||||
|
onMount(async () => { |
||||||
|
await loadBinaries(); |
||||||
|
}); |
||||||
|
|
||||||
|
async function loadBinaries() { |
||||||
|
$isLoading = true; |
||||||
|
try { |
||||||
|
$binariesData = await fetchBinaries($userSigner, $userPubkey); |
||||||
|
$error = ''; |
||||||
|
} catch (e) { |
||||||
|
$error = e.message; |
||||||
|
} finally { |
||||||
|
$isLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function handleUpdate() { |
||||||
|
// Filter out empty URLs |
||||||
|
const filteredUrls = {}; |
||||||
|
for (const [name, url] of Object.entries(urls)) { |
||||||
|
if (url.trim()) { |
||||||
|
filteredUrls[name] = url.trim(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!version.trim()) { |
||||||
|
$error = 'Version is required'; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (Object.keys(filteredUrls).length === 0) { |
||||||
|
$error = 'At least one binary URL is required'; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
isUpdating = true; |
||||||
|
updateResult = null; |
||||||
|
$error = ''; |
||||||
|
|
||||||
|
try { |
||||||
|
updateResult = await updateBinaries($userSigner, $userPubkey, version.trim(), filteredUrls); |
||||||
|
await loadBinaries(); |
||||||
|
} catch (e) { |
||||||
|
$error = e.message; |
||||||
|
} finally { |
||||||
|
isUpdating = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function handleRollback() { |
||||||
|
if (!confirm('Are you sure you want to rollback to the previous version?')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
isUpdating = true; |
||||||
|
$error = ''; |
||||||
|
|
||||||
|
try { |
||||||
|
const result = await rollbackVersion($userSigner, $userPubkey); |
||||||
|
updateResult = { |
||||||
|
success: true, |
||||||
|
message: `Rolled back from ${result.previous_version} to ${result.current_version}. Restart services to apply.` |
||||||
|
}; |
||||||
|
await loadBinaries(); |
||||||
|
} catch (e) { |
||||||
|
$error = e.message; |
||||||
|
} finally { |
||||||
|
isUpdating = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function setReleaseUrls() { |
||||||
|
// Helper to fill in URLs from a release base |
||||||
|
const baseUrl = prompt('Enter release base URL (e.g., https://git.mleku.dev/mleku/next.orly.dev/releases/download/v0.55.11):'); |
||||||
|
if (!baseUrl) return; |
||||||
|
|
||||||
|
const cleanBase = baseUrl.replace(/\/$/, ''); |
||||||
|
const arch = prompt('Enter architecture (amd64 or arm64):', 'amd64'); |
||||||
|
if (!arch) return; |
||||||
|
|
||||||
|
const ver = version.trim() || baseUrl.split('/').pop(); |
||||||
|
|
||||||
|
urls['orly'] = `${cleanBase}/orly-${ver.replace('v', '')}-linux-${arch}`; |
||||||
|
urls['orly-db-badger'] = `${cleanBase}/orly-db-badger-${ver.replace('v', '')}-linux-${arch}`; |
||||||
|
urls['orly-acl-follows'] = `${cleanBase}/orly-acl-follows-${ver.replace('v', '')}-linux-${arch}`; |
||||||
|
urls['orly-launcher'] = `${cleanBase}/orly-launcher-${ver.replace('v', '')}-linux-${arch}`; |
||||||
|
|
||||||
|
if (!version.trim()) { |
||||||
|
version = ver; |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="update-page"> |
||||||
|
<div class="page-header"> |
||||||
|
<h2>Update Binaries</h2> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if $error} |
||||||
|
<div class="error-banner">{$error}</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
{#if updateResult?.success} |
||||||
|
<div class="success-banner"> |
||||||
|
{updateResult.message} |
||||||
|
{#if updateResult.downloaded_files?.length} |
||||||
|
<br>Downloaded: {updateResult.downloaded_files.join(', ')} |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<div class="current-version"> |
||||||
|
<h3>Current Version</h3> |
||||||
|
<div class="version-info"> |
||||||
|
<span class="version">{$binariesData?.current_version || 'unknown'}</span> |
||||||
|
<button |
||||||
|
class="rollback-btn" |
||||||
|
on:click={handleRollback} |
||||||
|
disabled={isUpdating || ($binariesData?.available_versions?.length || 0) < 2} |
||||||
|
> |
||||||
|
Rollback |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="update-form"> |
||||||
|
<h3>Install New Version</h3> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<label for="version">Version</label> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
id="version" |
||||||
|
bind:value={version} |
||||||
|
placeholder="v0.55.11" |
||||||
|
disabled={isUpdating} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<div class="url-header"> |
||||||
|
<label>Binary URLs</label> |
||||||
|
<button class="helper-btn" on:click={setReleaseUrls} disabled={isUpdating}> |
||||||
|
Fill from Release |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#each Object.keys(urls) as name} |
||||||
|
<div class="url-input"> |
||||||
|
<span class="binary-name">{name}</span> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
bind:value={urls[name]} |
||||||
|
placeholder="https://..." |
||||||
|
disabled={isUpdating} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
{/each} |
||||||
|
</div> |
||||||
|
|
||||||
|
<button |
||||||
|
class="update-btn" |
||||||
|
on:click={handleUpdate} |
||||||
|
disabled={isUpdating} |
||||||
|
> |
||||||
|
{isUpdating ? 'Updating...' : 'Download & Install'} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if $binariesData?.available_versions?.length} |
||||||
|
<div class="versions-list"> |
||||||
|
<h3>Installed Versions</h3> |
||||||
|
<table> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Version</th> |
||||||
|
<th>Installed</th> |
||||||
|
<th>Binaries</th> |
||||||
|
<th>Status</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{#each $binariesData.available_versions as ver} |
||||||
|
<tr class:current={ver.is_current}> |
||||||
|
<td class="version-cell">{ver.version}</td> |
||||||
|
<td>{new Date(ver.installed_at).toLocaleString()}</td> |
||||||
|
<td>{ver.binaries?.length || 0} files</td> |
||||||
|
<td> |
||||||
|
{#if ver.is_current} |
||||||
|
<span class="current-badge">Current</span> |
||||||
|
{/if} |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{/each} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.update-page { |
||||||
|
padding: 20px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header { |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.page-header h2 { |
||||||
|
font-size: 1.5rem; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.error-banner { |
||||||
|
background: #ffebee; |
||||||
|
color: #c62828; |
||||||
|
padding: 12px 16px; |
||||||
|
border-radius: 6px; |
||||||
|
margin-bottom: 20px; |
||||||
|
border: 1px solid #ffcdd2; |
||||||
|
} |
||||||
|
|
||||||
|
.success-banner { |
||||||
|
background: #e8f5e9; |
||||||
|
color: #2e7d32; |
||||||
|
padding: 12px 16px; |
||||||
|
border-radius: 6px; |
||||||
|
margin-bottom: 20px; |
||||||
|
border: 1px solid #c8e6c9; |
||||||
|
} |
||||||
|
|
||||||
|
.current-version, |
||||||
|
.update-form, |
||||||
|
.versions-list { |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 20px; |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: 1.1rem; |
||||||
|
color: var(--text-color); |
||||||
|
margin-bottom: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.version-info { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
|
.version { |
||||||
|
font-size: 1.5rem; |
||||||
|
font-weight: 600; |
||||||
|
font-family: monospace; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.rollback-btn { |
||||||
|
padding: 8px 16px; |
||||||
|
background: var(--warning); |
||||||
|
border: none; |
||||||
|
color: white; |
||||||
|
border-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.rollback-btn:hover:not(:disabled) { |
||||||
|
opacity: 0.9; |
||||||
|
} |
||||||
|
|
||||||
|
.rollback-btn:disabled { |
||||||
|
opacity: 0.5; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.form-group { |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.form-group > label { |
||||||
|
display: block; |
||||||
|
font-size: 0.9rem; |
||||||
|
color: var(--text-color); |
||||||
|
margin-bottom: 8px; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.form-group input[type="text"] { |
||||||
|
width: 100%; |
||||||
|
padding: 10px 12px; |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 4px; |
||||||
|
font-size: 0.95rem; |
||||||
|
background: var(--bg-color); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.form-group input:focus { |
||||||
|
outline: none; |
||||||
|
border-color: var(--primary); |
||||||
|
} |
||||||
|
|
||||||
|
.url-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
.url-header label { |
||||||
|
font-size: 0.9rem; |
||||||
|
color: var(--text-color); |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.helper-btn { |
||||||
|
padding: 4px 12px; |
||||||
|
font-size: 0.8rem; |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 4px; |
||||||
|
color: var(--text-color); |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.helper-btn:hover:not(:disabled) { |
||||||
|
background: var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
.url-input { |
||||||
|
display: flex; |
||||||
|
gap: 12px; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
.binary-name { |
||||||
|
width: 140px; |
||||||
|
font-family: monospace; |
||||||
|
font-size: 0.85rem; |
||||||
|
color: var(--muted-color); |
||||||
|
} |
||||||
|
|
||||||
|
.url-input input { |
||||||
|
flex: 1; |
||||||
|
padding: 8px 12px; |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 4px; |
||||||
|
font-size: 0.85rem; |
||||||
|
background: var(--bg-color); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.update-btn { |
||||||
|
width: 100%; |
||||||
|
padding: 12px; |
||||||
|
background: var(--primary); |
||||||
|
border: none; |
||||||
|
color: white; |
||||||
|
border-radius: 6px; |
||||||
|
font-size: 1rem; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.update-btn:hover:not(:disabled) { |
||||||
|
background: var(--primary-hover); |
||||||
|
} |
||||||
|
|
||||||
|
.update-btn:disabled { |
||||||
|
opacity: 0.5; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
width: 100%; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
th, td { |
||||||
|
padding: 10px 12px; |
||||||
|
text-align: left; |
||||||
|
border-bottom: 1px solid var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
font-size: 0.85rem; |
||||||
|
color: var(--muted-color); |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
td { |
||||||
|
font-size: 0.9rem; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.version-cell { |
||||||
|
font-family: monospace; |
||||||
|
} |
||||||
|
|
||||||
|
tr.current { |
||||||
|
background: rgba(0, 188, 212, 0.1); |
||||||
|
} |
||||||
|
|
||||||
|
.current-badge { |
||||||
|
background: var(--primary); |
||||||
|
color: white; |
||||||
|
padding: 2px 8px; |
||||||
|
border-radius: 4px; |
||||||
|
font-size: 0.75rem; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
import { writable } from 'svelte/store'; |
||||||
|
|
||||||
|
// Authentication state
|
||||||
|
export const isLoggedIn = writable(false); |
||||||
|
export const userPubkey = writable(''); |
||||||
|
export const userSigner = writable(null); |
||||||
|
export const authMethod = writable(''); // 'extension' or 'nsec'
|
||||||
|
|
||||||
|
// Status data
|
||||||
|
export const statusData = writable(null); |
||||||
|
export const configData = writable(null); |
||||||
|
export const binariesData = writable(null); |
||||||
|
|
||||||
|
// Loading states
|
||||||
|
export const isLoading = writable(false); |
||||||
|
export const error = writable(''); |
||||||
Loading…
Reference in new issue