feat: initial Wails v3 desktop framework

GUI packages, examples, and documentation for building
desktop applications with Go and web technologies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 08:44:23 +00:00
commit 032c426ac3
1032 changed files with 188455 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Dependencies
node_modules/
vendor/
# Build output
dist/
build/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Environment
.env
.env.local

View file

@ -0,0 +1 @@
3549a54018bb5a6aaa1fa1aeb5cc15ac

View file

@ -0,0 +1 @@
163aa3367c4384cf5394719f17981d03

View file

@ -0,0 +1 @@
c025c2e151bf4e6199c2a2cf6ab6e123

View file

@ -0,0 +1 @@
163aa3367c4384cf5394719f17981d03

View file

@ -0,0 +1 @@
4f04fa3bc224e7776143edb1fa8eec45

View file

@ -0,0 +1,42 @@
version: '3'
includes:
common: "./build/Taskfile.yml"
windows: "./build/windows/Taskfile.yml"
darwin: "./build/darwin/Taskfile.yml"
linux: "./build/linux/Taskfile.yml"
vars:
APP_NAME: "core-demo"
BIN_DIR: "./build/bin"
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
tasks:
build:
summary: Builds the application
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
cmds:
- task: "{{OS}}:package"
run:
summary: Runs the application
cmds:
- task: "{{OS}}:run"
# This is the main dev task called by the Makefile.
# It delegates to the actual wails command below.
dev:
summary: Runs the application in development mode
cmds:
- task: dev:wails
# This task contains the real wails dev command.
# This avoids the recursive loop and provides a clear target.
dev:wails:
internal: true
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}

View file

@ -0,0 +1,50 @@
{
"code": "mining",
"type": "app",
"name": "Mining Module",
"version": "0.1.0",
"namespace": "mining",
"description": "Cryptocurrency mining management",
"author": "Lethean",
"contexts": ["miner", "default"],
"menu": [
{
"id": "mining",
"label": "Mining",
"order": 200,
"contexts": ["miner"],
"children": [
{"id": "mining-dashboard", "label": "Dashboard", "route": "/mining/dashboard", "order": 1},
{"id": "mining-pools", "label": "Pools", "route": "/mining/pools", "order": 2},
{"id": "mining-sep1", "separator": true, "order": 3},
{"id": "mining-start", "label": "Start Mining", "action": "mining:start", "order": 4},
{"id": "mining-stop", "label": "Stop Mining", "action": "mining:stop", "order": 5}
]
}
],
"routes": [
{"path": "/mining/dashboard", "component": "mining-dashboard", "title": "Mining Dashboard", "contexts": ["miner"]},
{"path": "/mining/pools", "component": "mining-pools", "title": "Mining Pools", "contexts": ["miner"]}
],
"api": [
{"method": "GET", "path": "/status", "description": "Get mining status"},
{"method": "POST", "path": "/start", "description": "Start mining"},
{"method": "POST", "path": "/stop", "description": "Stop mining"},
{"method": "GET", "path": "/pools", "description": "List configured pools"}
],
"downloads": {
"x86_64": {
"darwin": {"url": "https://releases.example.com/mining/darwin-x86_64.tar.gz"},
"linux": {"url": "https://releases.example.com/mining/linux-x86_64.tar.gz"},
"windows": {"url": "https://releases.example.com/mining/windows-x86_64.zip"}
},
"aarch64": {
"darwin": {"url": "https://releases.example.com/mining/darwin-aarch64.tar.gz"}
}
},
"config": {
"defaultPool": "",
"threads": 0,
"intensity": 50
}
}

View file

@ -0,0 +1,157 @@
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// ClaudeBridge forwards messages between GUI clients and the MCP core WebSocket.
type ClaudeBridge struct {
mcpConn *websocket.Conn
mcpURL string
clients map[*websocket.Conn]bool
clientsMu sync.RWMutex
broadcast chan []byte
reconnectMu sync.Mutex
}
// NewClaudeBridge creates a new bridge to the MCP core WebSocket.
func NewClaudeBridge(mcpURL string) *ClaudeBridge {
return &ClaudeBridge{
mcpURL: mcpURL,
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan []byte, 256),
}
}
// Start connects to the MCP WebSocket and starts the bridge.
func (cb *ClaudeBridge) Start() {
go cb.connectToMCP()
go cb.broadcastLoop()
}
// connectToMCP establishes connection to the MCP core WebSocket.
func (cb *ClaudeBridge) connectToMCP() {
for {
cb.reconnectMu.Lock()
if cb.mcpConn != nil {
cb.mcpConn.Close()
}
log.Printf("Claude bridge connecting to MCP at %s", cb.mcpURL)
conn, _, err := websocket.DefaultDialer.Dial(cb.mcpURL, nil)
if err != nil {
log.Printf("Claude bridge failed to connect to MCP: %v", err)
cb.reconnectMu.Unlock()
time.Sleep(5 * time.Second)
continue
}
cb.mcpConn = conn
cb.reconnectMu.Unlock()
log.Printf("Claude bridge connected to MCP")
// Read messages from MCP and broadcast to clients
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("Claude bridge MCP read error: %v", err)
break
}
cb.broadcast <- message
}
// Connection lost, retry
time.Sleep(2 * time.Second)
}
}
// broadcastLoop sends messages from MCP to all connected clients.
func (cb *ClaudeBridge) broadcastLoop() {
for message := range cb.broadcast {
cb.clientsMu.RLock()
for client := range cb.clients {
err := client.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Claude bridge client write error: %v", err)
}
}
cb.clientsMu.RUnlock()
}
}
// HandleWebSocket handles WebSocket connections from GUI clients.
func (cb *ClaudeBridge) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Claude bridge upgrade error: %v", err)
return
}
cb.clientsMu.Lock()
cb.clients[conn] = true
cb.clientsMu.Unlock()
// Send connected message
connMsg, _ := json.Marshal(map[string]any{
"type": "system",
"data": "Connected to Claude bridge",
"timestamp": time.Now(),
})
conn.WriteMessage(websocket.TextMessage, connMsg)
defer func() {
cb.clientsMu.Lock()
delete(cb.clients, conn)
cb.clientsMu.Unlock()
conn.Close()
}()
// Read messages from client and forward to MCP
for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
// Parse the message to check type
var msg map[string]any
if err := json.Unmarshal(message, &msg); err != nil {
continue
}
// Forward claude_message to MCP
if msgType, ok := msg["type"].(string); ok && msgType == "claude_message" {
cb.sendToMCP(message)
}
}
}
// sendToMCP sends a message to the MCP WebSocket.
func (cb *ClaudeBridge) sendToMCP(message []byte) {
cb.reconnectMu.Lock()
defer cb.reconnectMu.Unlock()
if cb.mcpConn == nil {
log.Printf("Claude bridge: MCP not connected")
return
}
err := cb.mcpConn.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Claude bridge MCP write error: %v", err)
}
}

120
cmd/core-demo/go.mod Normal file
View file

@ -0,0 +1,120 @@
module core-gui
go 1.25.5
require (
github.com/host-uk/core v0.0.0-00010101000000-000000000000
github.com/host-uk/core/pkg/display v0.0.0
github.com/host-uk/core/pkg/mcp v0.0.0-00010101000000-000000000000
github.com/host-uk/core/pkg/webview v0.0.0-00010101000000-000000000000
github.com/host-uk/core/pkg/ws v0.0.0-00010101000000-000000000000
github.com/gorilla/websocket v1.5.3
github.com/wailsapp/wails/v3 v3.0.0-alpha.41
)
replace (
github.com/host-uk/core => ../../
github.com/host-uk/core/pkg/config => ../../pkg/config
github.com/host-uk/core/pkg/core => ../../pkg/core
github.com/host-uk/core/pkg/crypt => ../../pkg/crypt
github.com/host-uk/core/pkg/display => ../../pkg/display
github.com/host-uk/core/pkg/docs => ../../pkg/docs
github.com/host-uk/core/pkg/help => ../../pkg/help
github.com/host-uk/core/pkg/i18n => ../../pkg/i18n
github.com/host-uk/core/pkg/ide => ../../pkg/ide
github.com/host-uk/core/pkg/io => ../../pkg/io
github.com/host-uk/core/pkg/mcp => ../../pkg/mcp
github.com/host-uk/core/pkg/module => ../../pkg/module
github.com/host-uk/core/pkg/plugin => ../../pkg/plugin
github.com/host-uk/core/pkg/process => ../../pkg/process
github.com/host-uk/core/pkg/runtime => ../../pkg/runtime
github.com/host-uk/core/pkg/webview => ../../pkg/webview
github.com/host-uk/core/pkg/workspace => ../../pkg/workspace
github.com/host-uk/core/pkg/ws => ../../pkg/ws
)
require (
dario.cat/mergo v1.0.2 // indirect
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/host-uk/core/pkg/config v0.0.0-00010101000000-000000000000 // indirect
github.com/host-uk/core/pkg/core v0.0.0 // indirect
github.com/host-uk/core/pkg/docs v0.0.0-00010101000000-000000000000 // indirect
github.com/host-uk/core/pkg/help v0.0.0-00010101000000-000000000000 // indirect
github.com/host-uk/core/pkg/i18n v0.0.0-00010101000000-000000000000 // indirect
github.com/host-uk/core/pkg/ide v0.0.0-00010101000000-000000000000 // indirect
github.com/host-uk/core/pkg/module v0.0.0-00010101000000-000000000000 // indirect
github.com/host-uk/core/pkg/process v0.0.0-00010101000000-000000000000 // indirect
github.com/Snider/Enchantrix v0.0.2 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lmittmann/tint v1.1.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modelcontextprotocol/go-sdk v1.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

235
cmd/core-demo/go.sum Normal file
View file

@ -0,0 +1,235 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/Snider/Enchantrix v0.0.2 h1:ExZQiBhfS/p/AHFTKhY80TOd+BXZjK95EzByAEgwvjs=
github.com/Snider/Enchantrix v0.0.2/go.mod h1:CtFcLAvnDT1KcuF1JBb/DJj0KplY8jHryO06KzQ1hsQ=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.41 h1:DYcC1/vtO862sxnoyCOMfLLypbzpFWI257fR6zDYY+Y=
github.com/wailsapp/wails/v3 v3.0.0-alpha.41/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

73
cmd/core-demo/main.go Normal file
View file

@ -0,0 +1,73 @@
package main
import (
"embed"
"io/fs"
"log"
core "github.com/host-uk/core"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)
//go:embed all:frontend/dist/frontend/browser
var assets embed.FS
// Default MCP port for the embedded server
const mcpPort = 9877
func main() {
// Create the Core runtime with plugin support
rt, err := core.NewRuntime()
if err != nil {
log.Fatal(err)
}
// Create the notifications service for native system notifications
notifier := notifications.New()
// Wire the notifier to the display service for native notifications
rt.Display.SetNotifier(notifier)
// Create the MCP bridge for Claude Code integration
// This provides WebView access, console capture, window control, and process management
mcpBridge := NewMCPBridge(mcpPort, rt.Display)
// Collect all services including plugins
// Display service registered separately so Wails calls its Startup() for tray/window
services := []application.Service{
application.NewService(rt.Runtime),
application.NewService(rt.Display),
application.NewService(notifier), // Native notifications
application.NewService(rt.Docs),
application.NewService(rt.Config),
application.NewService(rt.I18n),
application.NewService(rt.Help),
application.NewService(rt.Crypt),
application.NewService(rt.IDE),
application.NewService(rt.Module),
application.NewService(rt.Workspace),
application.NewService(mcpBridge), // MCP Bridge for Claude Code
}
services = append(services, rt.PluginServices()...)
// Strip the embed path prefix so files are served from root
staticAssets, err := fs.Sub(assets, "frontend/dist/frontend/browser")
if err != nil {
log.Fatal(err)
}
app := application.New(application.Options{
Services: services,
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(staticAssets),
},
})
log.Printf("Starting Core GUI with MCP server on port %d", mcpPort)
err = app.Run()
if err != nil {
log.Fatal(err)
}
}

1135
cmd/core-demo/mcp_bridge.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
console.log("Hello from app.js!");

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,9 @@
//@ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
Object.freeze($Create.Events);

View file

@ -0,0 +1,2 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

View file

@ -0,0 +1,4 @@
// This is the main entry point for the frontend application.
// We can add application-specific JavaScript here in the future.
console.log("Core Framework App Loaded");

View file

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Core Framework</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'core-gray': {
'light': '#333333',
'DEFAULT': '#1a1a1a',
'dark': '#0d0d0d',
},
'core-blue': '#00aaff',
}
}
}
}
</script>
<style>
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
</head>
<body class="bg-core-gray-dark text-gray-300 flex items-center justify-center min-h-screen font-sans">
<div class="w-full max-w-4xl mx-auto p-8 text-center">
<header class="mb-12">
<h1 class="text-5xl font-bold text-white mb-2">
<span class="text-core-blue">Core</span> Framework
</h1>
<p class="text-lg text-gray-400">A modular foundation for building robust desktop applications.</p>
</header>
<main>
<h2 class="text-3xl font-semibold text-white mb-8">Loaded Services</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Config Service Card -->
<div class="bg-core-gray p-6 rounded-lg shadow-lg border border-core-gray-light transform hover:scale-105 transition-transform duration-300">
<div class="text-core-blue mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-2">Config</h3>
<p class="text-gray-400">Manages application state and user preferences.</p>
</div>
<!-- Display Service Card -->
<div class="bg-core-gray p-6 rounded-lg shadow-lg border border-core-gray-light transform hover:scale-105 transition-transform duration-300">
<div class="text-core-blue mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-2">Display</h3>
<p class="text-gray-400">Controls windows, menus, and system tray interactions.</p>
</div>
<!-- Crypt Service Card -->
<div class="bg-core-gray p-6 rounded-lg shadow-lg border border-core-gray-light transform hover:scale-105 transition-transform duration-300">
<div class="text-core-blue mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-2">Crypt</h3>
<p class="text-gray-400">Provides cryptographic functions and security.</p>
</div>
</div>
</main>
</div>
<script type="module" src="assets/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Core System Tray</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'core-gray': {
'light': '#333333',
'DEFAULT': '#1a1a1a',
'dark': '#0d0d0d',
},
'core-blue': '#00aaff',
}
}
}
}
</script>
<style>
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Ensure no scrollbars appear in the small window */
overflow: hidden;
}
</style>
</head>
<body class="bg-core-gray-dark text-gray-300 font-sans">
<div class="w-full h-full p-4 text-center">
<header class="mb-6">
<h1 class="text-3xl font-bold text-white">
<span class="text-core-blue">Core</span> Status
</h1>
<p class="text-sm text-gray-400">Services at a glance.</p>
</header>
<main>
<div class="space-y-4">
<!-- Config Service Card -->
<div class="bg-core-gray p-4 rounded-lg shadow-md border border-core-gray-light flex items-center space-x-4">
<div class="text-core-blue">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div class="text-left">
<h3 class="text-lg font-bold text-white">Config</h3>
<p class="text-sm text-gray-400">State and preferences loaded.</p>
</div>
</div>
<!-- Display Service Card -->
<div class="bg-core-gray p-4 rounded-lg shadow-md border border-core-gray-light flex items-center space-x-4">
<div class="text-core-blue">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div class="text-left">
<h3 class="text-lg font-bold text-white">Display</h3>
<p class="text-sm text-gray-400">UI manager is active.</p>
</div>
</div>
<!-- Crypt Service Card -->
<div class="bg-core-gray p-4 rounded-lg shadow-md border border-core-gray-light flex items-center space-x-4">
<div class="text-core-blue">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<div class="text-left">
<h3 class="text-lg font-bold text-white">Crypt</h3>
<p class="text-sm text-gray-400">Security services are running.</p>
</div>
</div>
</div>
</main>
<footer class="absolute bottom-4 left-0 right-0 text-center">
<p class="text-xs text-gray-500">Core Framework Active</p>
</footer>
</div>
<script src="assets/app.js"></script>
</body>
</html>

1084
cmd/core-demo/public/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
{
"name": "core-app",
"version": "0.0.0",
"scripts": {
"start": "vite",
"build": "vite build",
"build:dev": "vite build --mode development"
},
"dependencies": {
"@tailwindplus/elements": "^1.0.18",
"@wailsio/runtime": "^3.0.0-alpha.72"
},
"devDependencies": {
"tailwindcss": "^4.1.14",
"vite": "^7.1.12"
}
}

View file

@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
// Set the root of the project to the 'html' directory.
// Vite will look for index.html in this folder.
root: 'html',
build: {
// Set the output directory for the build.
// We need to go up one level from 'html' to place the 'dist' folder
// in the correct location for Wails.
outDir: '../dist',
// Ensure the output directory is emptied before each build.
emptyOutDir: true,
},
});

View file

@ -0,0 +1 @@
e803e4518c821ae848c7ec1f47f0a084

View file

@ -0,0 +1 @@
d9e614eea705157b1140aedf3b26036f

View file

@ -0,0 +1 @@
c025c2e151bf4e6199c2a2cf6ab6e123

View file

@ -0,0 +1 @@
d9e614eea705157b1140aedf3b26036f

View file

@ -0,0 +1 @@
7e8f6eccf37e42c5cc2be6fe02ef76b9

42
cmd/core-gui/Taskfile.yml Normal file
View file

@ -0,0 +1,42 @@
version: '3'
includes:
common: "./build/Taskfile.yml"
windows: "./build/windows/Taskfile.yml"
darwin: "./build/darwin/Taskfile.yml"
linux: "./build/linux/Taskfile.yml"
vars:
APP_NAME: "core-gui"
BIN_DIR: "./build/bin"
VITE_PORT: '{{.WAILS_VITE_PORT | default 9246}}'
tasks:
build:
summary: Builds the application
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
cmds:
- task: "{{OS}}:package"
run:
summary: Runs the application
cmds:
- task: "{{OS}}:run"
# This is the main dev task called by the Makefile.
# It delegates to the actual wails command below.
dev:
summary: Runs the application in development mode
cmds:
- task: dev:wails
# This task contains the real wails dev command.
# This avoids the recursive loop and provides a clear target.
dev:wails:
internal: true
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}

View file

@ -0,0 +1,50 @@
{
"code": "mining",
"type": "app",
"name": "Mining Module",
"version": "0.1.0",
"namespace": "mining",
"description": "Cryptocurrency mining management",
"author": "Lethean",
"contexts": ["miner", "default"],
"menu": [
{
"id": "mining",
"label": "Mining",
"order": 200,
"contexts": ["miner"],
"children": [
{"id": "mining-dashboard", "label": "Dashboard", "route": "/mining/dashboard", "order": 1},
{"id": "mining-pools", "label": "Pools", "route": "/mining/pools", "order": 2},
{"id": "mining-sep1", "separator": true, "order": 3},
{"id": "mining-start", "label": "Start Mining", "action": "mining:start", "order": 4},
{"id": "mining-stop", "label": "Stop Mining", "action": "mining:stop", "order": 5}
]
}
],
"routes": [
{"path": "/mining/dashboard", "component": "mining-dashboard", "title": "Mining Dashboard", "contexts": ["miner"]},
{"path": "/mining/pools", "component": "mining-pools", "title": "Mining Pools", "contexts": ["miner"]}
],
"api": [
{"method": "GET", "path": "/status", "description": "Get mining status"},
{"method": "POST", "path": "/start", "description": "Start mining"},
{"method": "POST", "path": "/stop", "description": "Stop mining"},
{"method": "GET", "path": "/pools", "description": "List configured pools"}
],
"downloads": {
"x86_64": {
"darwin": {"url": "https://releases.example.com/mining/darwin-x86_64.tar.gz"},
"linux": {"url": "https://releases.example.com/mining/linux-x86_64.tar.gz"},
"windows": {"url": "https://releases.example.com/mining/windows-x86_64.zip"}
},
"aarch64": {
"darwin": {"url": "https://releases.example.com/mining/darwin-aarch64.tar.gz"}
}
},
"config": {
"defaultPool": "",
"threads": 0,
"intensity": 50
}
}

View file

@ -0,0 +1,157 @@
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// ClaudeBridge forwards messages between GUI clients and the MCP core WebSocket.
type ClaudeBridge struct {
mcpConn *websocket.Conn
mcpURL string
clients map[*websocket.Conn]bool
clientsMu sync.RWMutex
broadcast chan []byte
reconnectMu sync.Mutex
}
// NewClaudeBridge creates a new bridge to the MCP core WebSocket.
func NewClaudeBridge(mcpURL string) *ClaudeBridge {
return &ClaudeBridge{
mcpURL: mcpURL,
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan []byte, 256),
}
}
// Start connects to the MCP WebSocket and starts the bridge.
func (cb *ClaudeBridge) Start() {
go cb.connectToMCP()
go cb.broadcastLoop()
}
// connectToMCP establishes connection to the MCP core WebSocket.
func (cb *ClaudeBridge) connectToMCP() {
for {
cb.reconnectMu.Lock()
if cb.mcpConn != nil {
cb.mcpConn.Close()
}
log.Printf("Claude bridge connecting to MCP at %s", cb.mcpURL)
conn, _, err := websocket.DefaultDialer.Dial(cb.mcpURL, nil)
if err != nil {
log.Printf("Claude bridge failed to connect to MCP: %v", err)
cb.reconnectMu.Unlock()
time.Sleep(5 * time.Second)
continue
}
cb.mcpConn = conn
cb.reconnectMu.Unlock()
log.Printf("Claude bridge connected to MCP")
// Read messages from MCP and broadcast to clients
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("Claude bridge MCP read error: %v", err)
break
}
cb.broadcast <- message
}
// Connection lost, retry
time.Sleep(2 * time.Second)
}
}
// broadcastLoop sends messages from MCP to all connected clients.
func (cb *ClaudeBridge) broadcastLoop() {
for message := range cb.broadcast {
cb.clientsMu.RLock()
for client := range cb.clients {
err := client.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Claude bridge client write error: %v", err)
}
}
cb.clientsMu.RUnlock()
}
}
// HandleWebSocket handles WebSocket connections from GUI clients.
func (cb *ClaudeBridge) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Claude bridge upgrade error: %v", err)
return
}
cb.clientsMu.Lock()
cb.clients[conn] = true
cb.clientsMu.Unlock()
// Send connected message
connMsg, _ := json.Marshal(map[string]any{
"type": "system",
"data": "Connected to Claude bridge",
"timestamp": time.Now(),
})
conn.WriteMessage(websocket.TextMessage, connMsg)
defer func() {
cb.clientsMu.Lock()
delete(cb.clients, conn)
cb.clientsMu.Unlock()
conn.Close()
}()
// Read messages from client and forward to MCP
for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
// Parse the message to check type
var msg map[string]any
if err := json.Unmarshal(message, &msg); err != nil {
continue
}
// Forward claude_message to MCP
if msgType, ok := msg["type"].(string); ok && msgType == "claude_message" {
cb.sendToMCP(message)
}
}
}
// sendToMCP sends a message to the MCP WebSocket.
func (cb *ClaudeBridge) sendToMCP(message []byte) {
cb.reconnectMu.Lock()
defer cb.reconnectMu.Unlock()
if cb.mcpConn == nil {
log.Printf("Claude bridge: MCP not connected")
return
}
err := cb.mcpConn.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("Claude bridge MCP write error: %v", err)
}
}

View file

@ -0,0 +1,7 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore
.env
.git
.gitignore

View file

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
cmd/core-gui/frontend.old/.gitignore vendored Normal file
View file

@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
.npmrc
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View file

@ -0,0 +1,69 @@
### Installation
- `npm install` (install dependencies)
- `npm outdated` (verify dependency status)
### Development
- `npm run start`
- Visit http://localhost:4200
## Lint
- `npm run lint`
## Tests (headless-ready, no Chrome required)
- Unit/integration: `npm run test` (opens browser), or:
- Headless (uses Puppeteer Chromium): `npm run test:headless`
- Coverage report (HTML + text-summary): `npm run coverage`
- Coverage thresholds are enforced in Karma (≈80% statements/lines/functions, 70% branches for global). Adjust in `karma.conf.js` if needed.
### TDD workflow and test naming (Good/Bad/Ugly)
- Follow strict TDD:
1) Write failing tests from user stories + acceptance criteria
2) Implement minimal code to pass
3) Refactor
- Test case naming convention: each logical test should have three variants to clarify intent and data quality.
- Example helpers in `src/testing/gbu.ts`:
```ts
import { itGood, itBad, itUgly, trio } from 'src/testing/gbu';
itGood('saves profile', () => {/* valid data */});
itBad('saves profile', () => {/* incorrect data (edge) */});
itUgly('saves profile', () => {/* invalid data/conditions */});
// Or use trio
trio('process order', {
good: () => {/* ... */},
bad: () => {/* ... */},
ugly: () => {/* ... */},
});
```
- Do not modify router-outlet containers in tests/components.
### Standalone Angular 20+ patterns (migration notes)
- This app is moving to Angular standalone APIs. Prefer:
- Standalone components (`standalone: true`, add `imports: []` per component)
- `provideRouter(...)`, `provideHttpClient(...)`, `provideServiceWorker(...)` in `app.config.ts`
- Translation is configured via `app.config.ts` using `TranslateModule.forRoot(...)` and an HTTP loader.
- Legacy NgModules should be converted progressively. If an `NgModule` remains but is unrouted/unreferenced, keep it harmlessly until deletion is approved. Do not alter the main router-outlet page context panel.
### Web Awesome + Font Awesome (Pro)
- Both Font Awesome and Web Awesome are integrated. Do not remove. Web Awesome assets are copied via `angular.json` assets, and its base path is set at runtime in `app.ts`:
```ts
import('@awesome.me/webawesome').then(m => m.setBasePath('/assets/web-awesome'));
```
- CSS includes are defined in `angular.json` and `src/styles.css`.
### SSR and production
- Build (browser + server): `npm run build`
- Serve SSR bundle: `npm run serve` → http://localhost:4000
### Notes for other LLMs / contributors
- Respect the constraints:
- Do NOT edit the router-outlet main panel; pages/services are the focus
- Preserve existing functionality; do not remove Web Awesome/Font Awesome
- Use strict TDD and Good/Bad/Ugly naming for tests
- Keep or improve code coverage ≥ configured thresholds for changed files
- Use Angular 20+ standalone patterns; update `app.config.ts` for global providers.
- For tests, prefer headless runs via Puppeteer (no local Chrome needed).
### Author
- Author: danny

View file

@ -0,0 +1,132 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"lthn.io": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
},
{
"glob": "@awesome.me/webawesome/**/*.*",
"input": "node_modules/",
"output": "/"
},
"src/sitemap.xml",
"src/robots.txt"
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"src/styles.css"
],
"scripts": [],
"define": {
"import.meta.vitest": "undefined"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1MB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all",
"serviceWorker": "ngsw-config.json",
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
}
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "development"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "lthn.io:build:production"
},
"development": {
"buildTarget": "lthn.io:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing",
"src/test.ts"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"schematicCollections": [
"angular-eslint"
],
"analytics": false
}
}

View file

@ -0,0 +1,63 @@
// @ts-check
const eslint = require("@eslint/js");
const tseslint = require("typescript-eslint");
const angular = require("angular-eslint");
module.exports = tseslint.config(
{
files: ["**/*.ts"],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
...angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
"@angular-eslint/directive-selector": [
"error",
{
type: "attribute",
prefix: "app",
style: "camelCase",
},
],
"@angular-eslint/component-selector": [
"error",
{
type: "element",
prefix: "app",
style: "kebab-case",
},
],
"@angular-eslint/component-class-suffix": [
"error",
{
suffixes: ["", "Component"]
}
],
"@angular-eslint/prefer-inject": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
],
"no-undefined": "off",
"no-var": "error",
"prefer-const": "error",
"func-names": "error",
"id-length": "error",
"newline-before-return": "error",
"space-before-blocks": "error",
"no-alert": "error"
},
},
{
files: ["**/*.html"],
extends: [
...angular.configs.templateRecommended,
...angular.configs.templateAccessibility,
],
rules: {},
}
);

View file

@ -0,0 +1,61 @@
process.env.CHROME_BIN = process.env.CHROME_BIN || (function() {
try { return require('puppeteer').executablePath(); } catch { return undefined; }
})();
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution order
random: true
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/angular-starter'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
],
check: {
global: {
statements: 80,
branches: 70,
functions: 80,
lines: 80
}
}
},
reporters: ['progress', 'kjhtml'],
browsers: ['Chrome'],
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: [
'--headless',
'--disable-gpu',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--remote-debugging-port=9222'
]
}
},
restartOnFileChange: true
});
};

View file

@ -0,0 +1,30 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.csr.html",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
]
}

12685
cmd/core-gui/frontend.old/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
{
"name": "lthn.io",
"version": "20.3.2",
"scripts": {
"ng": "ng",
"dev": "ng serve --port 4200",
"start": "ng serve --port 4200",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"preview": "http-server ./dist/lthn-dns-web/browser -o",
"puppeteer:install": "npx puppeteer browsers install chrome || true",
"test": "ng test",
"test:headless": "(export CHROME_BIN=\"$(node -e \"console.log(require('puppeteer').executablePath())\")\"; ng test --no-watch --code-coverage=false --browsers=ChromeHeadless) || (npm run puppeteer:install && export CHROME_BIN=\"$(node -e \"console.log(require('puppeteer').executablePath())\")\" && ng test --no-watch --code-coverage=false --browsers=ChromeHeadless)",
"coverage": "(export CHROME_BIN=\"$(node -e \"console.log(require('puppeteer').executablePath())\")\"; ng test --no-watch --code-coverage --browsers=ChromeHeadless) || (npm run puppeteer:install && export CHROME_BIN=\"$(node -e \"console.log(require('puppeteer').executablePath())\")\" && ng test --no-watch --code-coverage --browsers=ChromeHeadless)",
"lint": "ng lint",
"serve": "node dist/angular-starter/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/common": "^20.3.2",
"@angular/compiler": "^20.3.2",
"@angular/core": "^20.3.2",
"@angular/forms": "^20.3.2",
"@angular/platform-browser": "^20.3.2",
"@angular/platform-server": "^20.3.2",
"@angular/router": "^20.3.2",
"@angular/service-worker": "^20.3.2",
"@angular/ssr": "^20.3.3",
"@awesome.me/kit-2e7e02d1b1": "^1.0.6",
"@awesome.me/webawesome": "file:~/Code/lib/webawesome",
"@fortawesome/fontawesome-free": "^7.0.1",
"@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0",
"bootstrap": "^5.3.8",
"express": "^5.1.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"zone.js": "^0.15.1"
},
"devDependencies": {
"@angular/build": "^20.3.3",
"@angular/cli": "^20.3.3",
"@angular/compiler-cli": "^20.3.2",
"@types/express": "^5.0.3",
"@types/jasmine": "^5.1.9",
"@types/node": "^24.6.0",
"angular-eslint": "^20.3.0",
"eslint": "^9.36.0",
"jasmine-core": "^5.11.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"puppeteer": "^23.7.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.45.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,331 @@
{ "app": {
"title": "Bob Wallet"
},
"sidebar": {
"wallet": "Wallet",
"topLevelDomains": "Top Level Domains",
"miscellaneous": "Miscellaneous",
"portfolio": "Portfolio",
"send": "Send",
"receive": "Receive",
"domainManager": "Domain Manager",
"browseDomains": "Browse Domains",
"yourBids": "Your Bids",
"watching": "Watching",
"exchange": "Exchange",
"claimAirdrop": "Claim Airdrop",
"signMessage": "Sign Message",
"verifyMessage": "Verify Message",
"currentHeight": "Current Height",
"currentHash": "Current Hash"
},
"topbar": {
"searchPlaceholder": "Search TLD",
"synced": "Synced",
"walletID": "Wallet ID",
"spendableBalance": "Spendable Balance",
"network": "Network",
"settings": "Settings",
"logout": "Logout"
},
"home": {
"spendable": "Spendable",
"locked": "Locked",
"revealable": "Revealable",
"redeemable": "Redeemable",
"registerable": "Registerable",
"renewable": "Renewable",
"transferring": "Transferring",
"finalizable": "Finalizable",
"inBids": "In bids",
"bid": "bid",
"bids": "bids",
"revealAll": "Reveal All",
"redeemAll": "Redeem All",
"registerAll": "Register All",
"renewAll": "Renew All",
"finalizeAll": "Finalize All",
"bidsReadyToReveal": "{{count}} bids ready to reveal",
"bidsReadyToRedeem": "{{count}} bids ready to redeem",
"namesReadyToRegister": "{{count}} names ready to register",
"domainsExpiringSoon": "{{count}} domains expiring soon",
"domainsInTransfer": "{{count}} domains in transfer",
"transfersReadyToFinalize": "{{count}} transfers ready to finalize",
"transactionHistory": "Transaction History",
"noTransactions": "No transactions yet. Transaction history will appear here once you start using the wallet."
},
"domainManager": {
"searchPlaceholder": "Search domains...",
"export": "Export",
"bulkTransfer": "Bulk Transfer",
"claimNamePayment": "Claim Name for Payment",
"emptyState": "You do not own any names yet.",
"browseDomainsLink": "Browse domains",
"toGetStarted": "to get started.",
"name": "Name",
"expires": "Expires",
"highestBid": "Highest Bid",
"showingDomains": "Showing {{count}} domains"
},
"exchange": {
"listings": "Listings",
"fills": "Fills",
"auctions": "Auctions",
"yourListings": "Your Listings",
"yourFills": "Your Fills",
"marketplaceAuctions": "Marketplace Auctions",
"createListing": "Create Listing",
"refresh": "Refresh",
"noActiveListings": "You have no active listings.",
"noFilledOrders": "You have no filled orders.",
"noActiveAuctions": "No active auctions found.",
"listDomainsInfo": "List your domains for sale to other Handshake users via the Shakedex protocol.",
"completedPurchasesInfo": "Completed purchases will appear here.",
"browseAuctionsInfo": "Browse available domain auctions from other users."
},
"searchTld": {
"searchPlaceholder": "Search for a name...",
"search": "Search",
"searching": "Searching...",
"enterNamePrompt": "Enter a name to search for availability and auction status.",
"available": "Available",
"inAuction": "In Auction",
"status": "Status",
"currentBid": "Current Bid",
"blocksUntilReveal": "Blocks until reveal",
"availableForBidding": "Available for bidding",
"auctionInProgress": "Auction in progress",
"placeBid": "Place Bid",
"watch": "Watch"
},
"onboarding": {
"welcome": "Welcome to Bob Wallet",
"setupPrompt": "Set up your wallet to start managing Handshake names",
"createNewWallet": "Create New Wallet",
"importSeed": "Import Seed",
"connectLedger": "Connect Ledger",
"important": "Important:",
"seedPhraseWarning": "Write down your seed phrase and store it in a secure location. You will need it to recover your wallet.",
"copySeedPhrase": "Copy Seed Phrase",
"savedSeed": "I've Saved My Seed",
"importSeedPrompt": "Enter your 12 or 24 word seed phrase to restore an existing wallet.",
"seedPhrase": "Seed Phrase",
"seedPhrasePlaceholder": "Enter your seed phrase",
"importWallet": "Import Wallet",
"ledgerPrompt": "Connect your Ledger hardware wallet to manage your Handshake names securely.",
"instructions": "Instructions:",
"ledgerStep1": "Connect your Ledger device via USB",
"ledgerStep2": "Enter your PIN on the device",
"ledgerStep3": "Open the Handshake app on your Ledger",
"ledgerStep4": "Click \"Connect\" below",
"connectLedgerButton": "Connect Ledger"
},
"settings": {
"general": "General",
"wallet": "Wallet",
"connection": "Connection",
"advanced": "Advanced",
"language": "Language",
"blockExplorer": "Block Explorer",
"theme": "Theme",
"light": "Light",
"dark": "Dark",
"system": "System",
"walletDirectory": "Wallet Directory",
"walletDirectoryInfo": "Location where wallet data is stored",
"changeDirectory": "Change Directory",
"backup": "Backup",
"backupInfo": "Export wallet seed phrase and settings",
"exportBackup": "Export Backup",
"rescanBlockchain": "Rescan Blockchain",
"rescanInfo": "Re-scan the blockchain for transactions",
"rescan": "Rescan",
"connectionType": "Connection Type",
"fullNode": "Full Node",
"spv": "SPV (Light)",
"customRPC": "Custom RPC",
"network": "Network",
"mainnet": "Mainnet",
"testnet": "Testnet",
"regtest": "Regtest",
"simnet": "Simnet",
"apiKey": "API Key",
"apiKeyInfo": "Node API authentication key",
"analytics": "Analytics",
"analyticsInfo": "Share anonymous usage data to improve Bob",
"developerOptions": "Developer Options",
"openDebugConsole": "Open Debug Console"
},
"common": {
"hns": "HNS",
"usd": "USD",
"loading": "Loading...",
"save": "Save",
"cancel": "Cancel",
"close": "Close",
"confirm": "Confirm",
"continue": "Continue",
"back": "Back",
"next": "Next",
"done": "Done",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info"
},
"app.boot.download-check": "Checking for Updates",
"app.boot.folder-check": "Setup Check",
"app.boot.loaded-runtime": "Application Loaded",
"app.boot.server-check": "Checking Server",
"app.boot.start-runtime": "Starting Desktop",
"app.core.ui.search": "Search",
"app.lthn.chain.daemons.lethean-blockchain-export": "Blockchain Export",
"app.lthn.chain.daemons.lethean-blockchain-import": "Blockchain Import",
"app.lthn.chain.daemons.lethean-wallet-cli": "Wallet CLI",
"app.lthn.chain.daemons.lethean-wallet-rpc": "Wallet RPC",
"app.lthn.chain.daemons.lethean-wallet-vpn-rpc": "Exit Node Wallet",
"app.lthn.chain.daemons.letheand": "Blockchain Service",
"app.lthn.chain.desc.no_transactions": "There were no transactions included in this block",
"app.lthn.chain.description": "Lethean (LTHN) Blockchain Stats",
"app.lthn.chain.heading": "Lethean Blockchain Stats",
"app.lthn.chain.menu.blocks": "Blocks",
"app.lthn.chain.menu.configuration": "Configuration",
"app.lthn.chain.menu.raw_data": "Raw Block Data",
"app.lthn.chain.menu.stats": "Stats",
"app.lthn.chain.table.age": "Age",
"app.lthn.chain.table.depth": "Depth",
"app.lthn.chain.table.difficulty": "Difficulty",
"app.lthn.chain.table.height": "Height",
"app.lthn.chain.table.reward": "Reward",
"app.lthn.chain.table.time": "Time",
"app.lthn.chain.table.title.chain-status": "Blockchain Status",
"app.lthn.chain.table.title.recent-blocks": "Recently Created Blocks",
"app.lthn.chain.title": "Blockchain Explorer",
"app.lthn.chain.words.alt_blocks_count": "Alt Blocks",
"app.lthn.chain.words.block_size": "Block Size",
"app.lthn.chain.words.block_size_limit": "Block Size Limit",
"app.lthn.chain.words.chain_stat": "Chain Stats",
"app.lthn.chain.words.chain_stat_value": "Node Reported Value",
"app.lthn.chain.words.cumulative_difficulty": "Cumulative Difficulty",
"app.lthn.chain.words.depth": "Depth from Top Block",
"app.lthn.chain.words.difficulty": "Difficulty",
"app.lthn.chain.words.grey_peerlist_size": "P2P Grey Peers",
"app.lthn.chain.words.hash": "Hash",
"app.lthn.chain.words.height": "Height",
"app.lthn.chain.words.incoming_connections_count": "P2P Incoming",
"app.lthn.chain.words.install-blockchain": "Install Blockchain",
"app.lthn.chain.words.last_block_time": "Synchronised to Block:",
"app.lthn.chain.words.loading-data": "Loading Blockchain Data",
"app.lthn.chain.words.major_version": "Major Version",
"app.lthn.chain.words.miner_transaction": "Miner Transaction",
"app.lthn.chain.words.miner_tx": "POW Miner Transaction",
"app.lthn.chain.words.minor_version": "Minor Version",
"app.lthn.chain.words.nonce": "Block Solution",
"app.lthn.chain.words.orphan_status": "Valid Block",
"app.lthn.chain.words.outgoing_connections_count": "P2P Out",
"app.lthn.chain.words.reward": "Reward",
"app.lthn.chain.words.start_time": "Start Time",
"app.lthn.chain.words.status": "Status",
"app.lthn.chain.words.target": "Target",
"app.lthn.chain.words.target_height": "Target Height",
"app.lthn.chain.words.testnet": "Testnet",
"app.lthn.chain.words.timestamp": "Timestamp",
"app.lthn.chain.words.top_height": "Newest Block",
"app.lthn.chain.words.tx_count": "Total Transactions",
"app.lthn.chain.words.tx_pool_size": "Pending Transactions",
"app.lthn.chain.words.unlock_time": "Unlock Block",
"app.lthn.chain.words.valid": "Valid Block",
"app.lthn.chain.words.version": "Block Structure Version",
"app.lthn.chain.words.white_peerlist_size": "P2P Whitelist",
"app.lthn.console.title": "Console",
"app.lthn.wallet.button.create-wallet": "Create Wallet",
"app.lthn.wallet.button.restore-wallet": "Restore Wallet",
"app.lthn.wallet.button.unlock-wallet": "Unlock",
"app.lthn.wallet.label.address": "Address",
"app.lthn.wallet.label.autosave": "Save Open Wallet",
"app.lthn.wallet.label.filename": "Filename",
"app.lthn.wallet.label.restore-height": "Restore Height",
"app.lthn.wallet.label.spend-key": "Spend Key",
"app.lthn.wallet.label.view-key": "View Key",
"app.lthn.wallet.label.wallet-password": "Wallet Password",
"app.lthn.wallet.label.wallet-password-confirm": "Confirm Password",
"app.lthn.wallet.titles.new-wallet": "Make New Wallet",
"app.lthn.wallet.titles.restore-keys": "Restore From Keys",
"app.lthn.wallet.titles.restore-seed": "Restore From Seed",
"app.lthn.wallet.titles.unlock-wallet": "Unlock Wallet",
"app.lthn.wallet.titles.wallet-transactions": "Wallet Transactions",
"app.market.apps": "App Marketplace",
"app.market.dashboard": "Dashboard",
"app.market.installed": "Installed Apps",
"app.market.no-apps-installed": "You have no apps installed.",
"app.market.view-installable-apps": "View Installable Apps",
"app.title": "Lethean Desktop",
"charts.network-hashrate.subtitle": "Data Provided by",
"charts.network-hashrate.title": "Network Hash Rate",
"lang.de": "German",
"lang.en": "English",
"lang.es": "Spanish",
"lang.fr": "French",
"lang.ru": "Russian",
"lang.uk": "Ukrainian (Ukraine)",
"lang.zh": "Chinese",
"menu.about": "About",
"menu.activity": "Activity",
"menu.api": "api",
"menu.blockchain": "Blockchain",
"menu.build": "Build",
"menu.dashboard": "Dashboard",
"menu.docs": "Documentation",
"menu.documentation": "Documentation",
"menu.explorer": "Explorer",
"menu.help": "Help",
"menu.hub-admin": "Admin Hub",
"menu.hub-client": "Client Hub",
"menu.hub-developer": "Developer",
"menu.hub-gateway": "Gateway",
"menu.hub-server": "Server Hub",
"menu.info": "info",
"menu.logout": "Sign Out",
"menu.mining": "Mining",
"menu.settings": "Settings",
"menu.vpn": "VPN",
"menu.wallet": "Wallet",
"menu.your-profile": "Your Profile",
"view.dashboard.description": "Lethean (LTHN) Web app",
"view.dashboard.heading": "Lethean Dashboard",
"view.dashboard.title": "Lethean (LTHN)",
"view.wallets.description": "Crypto Wallet Manager",
"view.wallets.heading": "Wallet Manager",
"view.wallets.title": "Wallets",
"words.actions.add": "Add",
"words.actions.clone": "Clone",
"words.actions.edit": "Edit",
"words.actions.install": "Install",
"words.actions.new": "New",
"words.actions.remove": "Remove",
"words.actions.report": "Report",
"words.actions.save": "Save",
"words.states.installing": "Installing",
"words.states.installing_desc": "We are downloading the blockchain executables from GitHub to your Lethean user directory.",
"words.states.loading": "Loading",
"words.states.not_installed": "Not Installed",
"words.states.not_installed_desc": "Click Install Blockchain to download the latest Lethean Blockchain CLI",
"words.things.button": "Button",
"words.things.documentation": "Documentation",
"words.things.menu": "Menu",
"words.things.mining-pool": "Mining Pool",
"words.things.page": "Page",
"words.things.problem": "Problem",
"words.things.type": "Type",
"words.time.past.day": "a day ago",
"words.time.past.days": "days ago",
"words.time.past.hour": "an hour ago",
"words.time.past.hours": "hours ago",
"words.time.past.minute": "a minute ago",
"words.time.past.minutes": "minutes ago",
"words.time.past.month": "a month ago",
"words.time.past.months": " months ago",
"words.time.past.seconds": "a few seconds ago",
"words.time.past.year": "a year ago",
"words.time.past.years": "years ago"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,57 @@
{
"name": "Core Framework",
"short_name": "Core",
"display": "standalone",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}

View file

@ -0,0 +1,3 @@
User-agent: *
Disallow:
Sitemap: /sitemap.xml

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
<url>
<loc>https://angular.ganatan.com/</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>1.00</priority>
</url>
<url>
<loc>https://angular.ganatan.com/about</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/contact</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/bootstrap</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/services</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/components</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/httpclient</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/forms</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://angular.ganatan.com/about/experience</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://angular.ganatan.com/about/skill</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://angular.ganatan.com/contact/mailing</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://angular.ganatan.com/contact/mapping</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://angular.ganatan.com/contact/website</loc>
<lastmod>2023-12-08T12:51:22+00:00</lastmod>
<priority>0.64</priority>
</url>
</urlset>

View file

@ -0,0 +1,15 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
import { TranslateLoader } from '@ngx-translate/core';
import { TranslateServerLoader } from './translate-server.loader';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes)),
{ provide: TranslateLoader, useClass: TranslateServerLoader }
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

View file

@ -0,0 +1,42 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { HttpClient, provideHttpClient, withFetch } from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';
import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';
import { routes } from './app.routes';
import { withInMemoryScrolling } from '@angular/router';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideServiceWorker } from '@angular/service-worker';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withFetch(),
),
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes,
withInMemoryScrolling({
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
}),
),
provideClientHydration(withEventReplay()),
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000'
}),
// Add ngx-translate providers
importProvidersFrom(
TranslateModule.forRoot({
fallbackLang: 'en'
})
),
provideTranslateHttpLoader({
prefix: './i18n/',
suffix: '.json'
})
]
};

View file

@ -0,0 +1,140 @@
<wa-page mobile-breakpoint="960">
<!-- Shell: left nav + content -->
<div class="app-shell"
style="display: grid; grid-template-columns: 280px 1fr; min-height: calc(100vh - 56px - 56px);">
<!-- Left navigation drawer (collapsible) -->
<wa-drawer>
<nav slot="navigation">
<a routerLink="/" class="sidebar-link">Home</a>
<a routerLink="/domain-manager" class="sidebar-link">Send</a>
<a routerLink="/settings" class="sidebar-link">Settings</a>
<a routerLink="/" class="icon-item active" aria-label="Dashboard">
<i class="fas fa-home"></i>
</a>
<a routerLink="/team" class="icon-item" aria-label="Team">
<i class="fas fa-users"></i>
</a>
<a routerLink="/projects" class="icon-item" aria-label="Projects">
<i class="fas fa-briefcase"></i>
</a>
<a routerLink="/calendar" class="icon-item" aria-label="Calendar">
<i class="fas fa-calendar-alt"></i>
</a>
<a routerLink="/documents" class="icon-item" aria-label="Documents">
<i class="fas fa-file-alt"></i>
</a>
<a routerLink="/reports" class="icon-item" aria-label="Reports">
<i class="fas fa-chart-pie"></i>
</a>
<!-- Add more nav items as needed -->
</nav>
</wa-drawer>
<main>
<router-outlet></router-outlet>
</main>
</div>
<!-- Footer -->
<app-footer slot="footer"></app-footer>
</wa-page>
<!--&lt;!&ndash; Top-level wrapper using Web Awesome (no Tailwind) &ndash;&gt;-->
<!--<div class="app-frame">-->
<!-- &lt;!&ndash; Mobile sidebar via Drawer &ndash;&gt;-->
<!-- <wa-drawer #mobileSidebar class="mobile-sidebar" placement="start" style="&#45;&#45;size: 20rem;">-->
<!-- <div class="sidebar-inner">-->
<!-- <div class="brand-row">-->
<!-- <img src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company" class="brand-logo" />-->
<!-- </div>-->
<!-- <nav class="nav-list">-->
<!-- <a routerLink="/" class="nav-item active">-->
<!-- <i class="fas fa-home"></i>-->
<!-- <span>Dashboard</span>-->
<!-- </a>-->
<!-- <a routerLink="/team" class="nav-item">-->
<!-- <i class="fas fa-users"></i>-->
<!-- <span>Team</span>-->
<!-- </a>-->
<!-- <a routerLink="/projects" class="nav-item">-->
<!-- <i class="fas fa-briefcase"></i>-->
<!-- <span>Projects</span>-->
<!-- </a>-->
<!-- <a routerLink="/calendar" class="nav-item">-->
<!-- <i class="fas fa-calendar-alt"></i>-->
<!-- <span>Calendar</span>-->
<!-- </a>-->
<!-- <a routerLink="/documents" class="nav-item">-->
<!-- <i class="fas fa-file-alt"></i>-->
<!-- <span>Documents</span>-->
<!-- </a>-->
<!-- <a routerLink="/reports" class="nav-item">-->
<!-- <i class="fas fa-chart-pie"></i>-->
<!-- <span>Reports</span>-->
<!-- </a>-->
<!-- </nav>-->
<!-- </div>-->
<!-- </wa-drawer>-->
<!-- &lt;!&ndash; Main column (left padding on lg for the fixed sidebar) &ndash;&gt;-->
<!-- <div class="main-col">-->
<!-- &lt;!&ndash; Topbar &ndash;&gt;-->
<!-- <header class="topbar">-->
<!-- <wa-button variant="text" size="small" class="lg-hidden" (click)="onToggleSidebar()" aria-label="Open sidebar">-->
<!-- <i class="fas fa-bars"></i>-->
<!-- </wa-button>-->
<!-- <div class="topbar-sep lg-hidden" aria-hidden="true"></div>-->
<!-- <div class="topbar-flex">-->
<!-- <form class="search-form" role="search">-->
<!-- <wa-input placeholder="Search" size="small" pill>-->
<!-- <i slot="prefix" class="fas fa-search"></i>-->
<!-- </wa-input>-->
<!-- </form>-->
<!-- <div class="topbar-actions">-->
<!-- <wa-button variant="text" size="small" aria-label="View notifications">-->
<!-- <i class="fas fa-bell"></i>-->
<!-- </wa-button>-->
<!-- <div class="lg-sep" aria-hidden="true"></div>-->
<!-- &lt;!&ndash; Profile dropdown &ndash;&gt;-->
<!-- <wa-dropdown>-->
<!-- <wa-button slot="trigger" size="small" pill>-->
<!-- <img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=facearea&facepad=2&w=48&h=48&q=80"-->
<!-- alt="" class="avatar" />-->
<!-- <span class="user-name lg-only">Tom Cook</span>-->
<!-- <i class="fas fa-chevron-down lg-only"></i>-->
<!-- </wa-button>-->
<!-- <wa-menu>-->
<!-- <wa-menu-item>Your profile</wa-menu-item>-->
<!-- <wa-menu-item>Sign out</wa-menu-item>-->
<!-- </wa-menu>-->
<!-- </wa-dropdown>-->
<!-- </div>-->
<!-- </div>-->
<!-- </header>-->
<!-- &lt;!&ndash; Primary content area (single Angular outlet) &ndash;&gt;-->
<!-- <main class="content">-->
<!-- <router-outlet></router-outlet>-->
<!-- </main>-->
<!-- </div>-->
<!-- &lt;!&ndash; Optional secondary aside on xl+ screens &ndash;&gt;-->
<!-- <aside class="secondary-aside">-->
<!-- &lt;!&ndash; Secondary column content (hidden on smaller screens) &ndash;&gt;-->
<!-- </aside>-->
<!-- &lt;!&ndash; Footer (existing component) &ndash;&gt;-->
<!-- <app-footer></app-footer>-->
<!--</div>-->

View file

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Prerender
}
];

View file

@ -0,0 +1,25 @@
import { Routes } from '@angular/router';
import { HomePage } from './pages/home/home.page';
import { SearchTldPage } from './pages/search-tld/search-tld.page';
import { OnboardingPage } from './pages/onboarding/onboarding.page';
import { SettingsPage } from './pages/settings/settings.page';
import { DomainManagerPage } from './pages/domain-manager/domain-manager.page';
import { ExchangePage } from './pages/exchange/exchange.page';
export const routes: Routes = [
{ path: '', redirectTo: '/account', pathMatch: 'full' },
{ path: 'account', component: HomePage, title: 'Portfolio • Bob Wallet' },
{ path: 'send', component: HomePage, title: 'Send • Bob Wallet' },
{ path: 'receive', component: HomePage, title: 'Receive • Bob Wallet' },
{ path: 'domain-manager', component: DomainManagerPage, title: 'Domain Manager • Bob Wallet' },
{ path: 'domains', component: SearchTldPage, title: 'Browse Domains • Bob Wallet' },
{ path: 'bids', component: HomePage, title: 'Your Bids • Bob Wallet' },
{ path: 'watching', component: HomePage, title: 'Watching • Bob Wallet' },
{ path: 'exchange', component: ExchangePage, title: 'Exchange • Bob Wallet' },
{ path: 'get-coins', component: HomePage, title: 'Claim Airdrop • Bob Wallet' },
{ path: 'sign-message', component: HomePage, title: 'Sign Message • Bob Wallet' },
{ path: 'verify-message', component: HomePage, title: 'Verify Message • Bob Wallet' },
{ path: 'settings', component: SettingsPage, title: 'Settings • Bob Wallet' },
{ path: 'onboarding', component: OnboardingPage, title: 'Onboarding • Bob Wallet' },
{ path: '**', redirectTo: '/account' }
];

View file

@ -0,0 +1,24 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
import { ActivatedRoute } from '@angular/router';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [
{
provide: ActivatedRoute,
useValue: {}
}
]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View file

@ -0,0 +1,40 @@
import { Component, OnInit, Inject, PLATFORM_ID, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef } from '@angular/core';
import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
import {RouterLink, RouterOutlet} from '@angular/router';
import { FooterComponent } from './shared/components/footer/footer.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import {Subscription} from 'rxjs';
@Component({
selector: 'app-root',
imports: [
CommonModule,
RouterOutlet,
FooterComponent,
TranslateModule,
RouterLink
],
templateUrl: './app.html',
styleUrl: './app.css',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class App {
@ViewChild('sidebar', { read: ElementRef, static: false }) sidebar?: ElementRef<HTMLElement>;
sidebarOpen = false;
userMenuOpen = false;
currentRole = 'Developer';
time: string = '';
constructor(
@Inject(DOCUMENT) private document: Document,
@Inject(PLATFORM_ID) private platformId: object,
private translateService: TranslateService
) {
// Set default language
this.translateService.use('en');
}
}

View file

@ -0,0 +1,63 @@
import { TestBed } from '@angular/core/testing';
import { Meta, Title } from '@angular/platform-browser';
import { SeoService } from './seo.service';
import { itGood, itBad, itUgly, trio } from 'src/testing/gbu';
describe('SeoService', () => {
let service: SeoService;
let metaSpy: jasmine.SpyObj<Meta>;
let titleSpy: jasmine.SpyObj<Title>;
beforeEach(() => {
metaSpy = jasmine.createSpyObj('Meta', ['updateTag']);
titleSpy = jasmine.createSpyObj('Title', ['setTitle']);
TestBed.configureTestingModule({
providers: [
SeoService,
{ provide: Meta, useValue: metaSpy },
{ provide: Title, useValue: titleSpy }
]
});
service = TestBed.inject(SeoService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('setMetaTitle', () => {
trio('sets document title', {
good: () => {
service.setMetaTitle('Hello');
expect(titleSpy.setTitle).toHaveBeenCalledOnceWith('Hello');
},
bad: () => {
service.setMetaTitle('');
expect(titleSpy.setTitle).toHaveBeenCalledWith('');
},
ugly: () => {
// Force invalid via any cast; ensure we do not throw
expect(() => service.setMetaTitle(null as any)).not.toThrow();
expect(titleSpy.setTitle).toHaveBeenCalledWith(null as any);
}
});
});
describe('setMetaDescription', () => {
itGood('updates description meta tag', () => {
service.setMetaDescription('desc');
expect(metaSpy.updateTag).toHaveBeenCalledWith({ name: 'description', content: 'desc' });
});
itBad('handles empty description', () => {
service.setMetaDescription('');
expect(metaSpy.updateTag).toHaveBeenCalledWith({ name: 'description', content: '' });
});
itUgly('does not throw on invalid description', () => {
expect(() => service.setMetaDescription(null as any)).not.toThrow();
expect(metaSpy.updateTag).toHaveBeenCalledWith({ name: 'description', content: null as any });
});
});
});

View file

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class SeoService {
constructor(
private meta: Meta,
private titleService: Title) {
}
public setMetaDescription(content: string) {
this.meta.updateTag(
{
name: 'description',
content: content
});
}
public setMetaTitle(title:string) {
this.titleService.setTitle(title);
}
}

View file

@ -0,0 +1,8 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { } from "@awesome.me/webawesome/dist/webawesome.loader.js"
// This module enables Angular to accept unknown custom elements (Web Awesome components)
// without throwing template parse errors.
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class CustomElementsModule {}

View file

@ -0,0 +1,81 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { DomainManagerPage } from './domain-manager.page';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('DomainManagerPage', () => {
let component: DomainManagerPage;
let fixture: ComponentFixture<DomainManagerPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DomainManagerPage, RouterTestingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(DomainManagerPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
// NEW: Verify component is an instance of DomainManagerPage
expect(component instanceof DomainManagerPage).toBe(true);
});
it('should initialize with empty searchQuery', () => {
expect(component.searchQuery).toBe('');
// NEW: Verify searchQuery is a string type
expect(typeof component.searchQuery).toBe('string');
});
it('should initialize with empty domains array', () => {
expect(component.domains).toEqual([]);
// NEW: Verify domains is an array
expect(Array.isArray(component.domains)).toBe(true);
});
it('should render domain manager header', () => {
const compiled = fixture.nativeElement as HTMLElement;
const header = compiled.querySelector('.domain-manager__header');
expect(header).toBeTruthy();
// NEW: Verify header contains actions section
const actions = header?.querySelector('.domain-manager__actions');
expect(actions).toBeTruthy();
});
it('should display search input', () => {
const compiled = fixture.nativeElement as HTMLElement;
const searchInput = compiled.querySelector('wa-input');
expect(searchInput).toBeTruthy();
// NEW: Verify search input has placeholder attribute
expect(searchInput?.hasAttribute('placeholder')).toBe(true);
});
it('should show empty state when no domains', () => {
const compiled = fixture.nativeElement as HTMLElement;
const emptyState = compiled.querySelector('.domain-manager__empty');
expect(emptyState).toBeTruthy();
expect(emptyState?.textContent).toContain('You do not own any names yet');
// NEW: Verify empty state is within a callout
const callout = compiled.querySelector('wa-callout');
expect(callout).toBeTruthy();
});
it('should render action buttons in header', () => {
const compiled = fixture.nativeElement as HTMLElement;
const buttons = compiled.querySelectorAll('.domain-manager__actions wa-button');
expect(buttons.length).toBeGreaterThan(0);
// NEW: Verify exactly 3 action buttons (Export, Bulk Transfer, Claim Name)
expect(buttons.length).toBe(3);
});
it('should have viewDomain method', () => {
spyOn(console, 'log');
component.viewDomain('testdomain');
expect(console.log).toHaveBeenCalledWith('View domain:', 'testdomain');
// NEW: Verify viewDomain is a function
expect(typeof component.viewDomain).toBe('function');
});
});

View file

@ -0,0 +1,107 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'app-domain-manager-page',
imports: [FormsModule, TranslateModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="domain-manager">
<div class="domain-manager__header">
<wa-input [placeholder]="'domainManager.searchPlaceholder' | translate" style="flex: 1; max-width: 400px;">
<input slot="input" [(ngModel)]="searchQuery" />
</wa-input>
<div class="domain-manager__actions">
<wa-button size="small" variant="neutral">
<wa-icon slot="prefix" name="fa-solid fa-download"></wa-icon>
{{ 'domainManager.export' | translate }}
</wa-button>
<wa-button size="small" variant="neutral">
<wa-icon slot="prefix" name="fa-solid fa-arrow-right-arrow-left"></wa-icon>
{{ 'domainManager.bulkTransfer' | translate }}
</wa-button>
<wa-button size="small" variant="primary">
<wa-icon slot="prefix" name="fa-solid fa-receipt"></wa-icon>
{{ 'domainManager.claimNamePayment' | translate }}
</wa-button>
</div>
</div>
@if (domains.length === 0) {
<div class="domain-manager__content">
<wa-callout variant="neutral">
<div class="domain-manager__empty">
<wa-icon name="fa-solid fa-folder-open" style="font-size: 3rem; opacity: 0.3; margin-bottom: 1rem;"></wa-icon>
<p>{{ 'domainManager.emptyState' | translate }}</p>
<p style="margin-top: 0.5rem;">
<a routerLink="/domains" style="color: var(--wa-color-primary-600); text-decoration: underline;">{{ 'domainManager.browseDomainsLink' | translate }}</a> {{ 'domainManager.toGetStarted' | translate }}
</p>
</div>
</wa-callout>
</div>
}
@if (domains.length > 0) {
<div class="domain-manager__content">
<div class="domain-manager__table">
<div class="table-header">
<div class="table-col">{{ 'domainManager.name' | translate }}</div>
<div class="table-col">{{ 'domainManager.expires' | translate }}</div>
<div class="table-col">{{ 'domainManager.highestBid' | translate }}</div>
</div>
@for (domain of domains; track domain) {
<div class="table-row" (click)="viewDomain(domain.name)">
<div class="table-col">{{domain.name}}/</div>
<div class="table-col">{{domain.expires}}</div>
<div class="table-col">{{domain.highestBid}} {{ 'common.hns' | translate }}</div>
</div>
}
</div>
<div class="domain-manager__pagination">
<wa-select size="small" value="10" style="width: 80px;">
<wa-option value="5">5</wa-option>
<wa-option value="10">10</wa-option>
<wa-option value="20">20</wa-option>
<wa-option value="50">50</wa-option>
</wa-select>
<span class="pagination-info">{{ 'domainManager.showingDomains' | translate: {count: domains.length} }}</span>
</div>
</div>
}
</div>
`,
styles: [`
.domain-manager { display: flex; flex-direction: column; gap: 1.5rem; }
.domain-manager__header { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
.domain-manager__actions { display: flex; gap: 0.5rem; }
.domain-manager__content { }
.domain-manager__empty { text-align: center; padding: 2rem; }
.domain-manager__empty p { margin: 0; color: var(--wa-color-neutral-600, #4b5563); }
.domain-manager__table { border: 1px solid var(--wa-color-neutral-200, #e5e7eb); border-radius: 0.5rem; overflow: hidden; }
.table-header { display: grid; grid-template-columns: 2fr 1fr 1fr; background: var(--wa-color-neutral-50, #fafafa); font-weight: 600; font-size: 0.875rem; }
.table-row { display: grid; grid-template-columns: 2fr 1fr 1fr; border-top: 1px solid var(--wa-color-neutral-200, #e5e7eb); cursor: pointer; transition: background 0.15s; }
.table-row:hover { background: var(--wa-color-neutral-50, #fafafa); }
.table-col { padding: 0.75rem 1rem; }
.domain-manager__pagination { display: flex; align-items: center; gap: 1rem; margin-top: 1rem; }
.pagination-info { font-size: 0.875rem; color: var(--wa-color-neutral-600, #4b5563); }
`]
})
export class DomainManagerPage {
searchQuery = '';
domains: any[] = [];
constructor(private router: Router) {}
viewDomain(name: string) {
// Navigate to individual domain view
console.log('View domain:', name);
}
}

View file

@ -0,0 +1,117 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExchangePage } from './exchange.page';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('ExchangePage', () => {
let component: ExchangePage;
let fixture: ComponentFixture<ExchangePage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ExchangePage],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(ExchangePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
// NEW: Verify component is an instance of ExchangePage
expect(component instanceof ExchangePage).toBe(true);
});
it('should render exchange container', () => {
const compiled = fixture.nativeElement as HTMLElement;
const exchange = compiled.querySelector('.exchange');
expect(exchange).toBeTruthy();
// NEW: Verify exchange container has tab group
const tabGroup = exchange?.querySelector('wa-tab-group');
expect(tabGroup).toBeTruthy();
});
it('should render tab group', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabGroup = compiled.querySelector('wa-tab-group');
expect(tabGroup).toBeTruthy();
// NEW: Verify tab group contains tabs
const tabs = tabGroup?.querySelectorAll('wa-tab');
expect(tabs?.length).toBeGreaterThan(0);
});
it('should render three tabs', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabs = compiled.querySelectorAll('wa-tab');
expect(tabs.length).toBe(3);
// NEW: Verify corresponding tab panels exist
const tabPanels = compiled.querySelectorAll('wa-tab-panel');
expect(tabPanels.length).toBe(3);
});
it('should have Listings tab', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabs = Array.from(compiled.querySelectorAll('wa-tab'));
const listingsTab = tabs.find(tab => tab.textContent?.includes('Listings'));
expect(listingsTab).toBeTruthy();
// NEW: Verify listings tab has correct panel attribute
expect(listingsTab?.getAttribute('panel')).toBe('listings');
});
it('should have Fills tab', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabs = Array.from(compiled.querySelectorAll('wa-tab'));
const fillsTab = tabs.find(tab => tab.textContent?.includes('Fills'));
expect(fillsTab).toBeTruthy();
// NEW: Verify fills tab has correct panel attribute
expect(fillsTab?.getAttribute('panel')).toBe('fills');
});
it('should have Auctions tab', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabs = Array.from(compiled.querySelectorAll('wa-tab'));
const auctionsTab = tabs.find(tab => tab.textContent?.includes('Auctions'));
expect(auctionsTab).toBeTruthy();
// NEW: Verify auctions tab has correct panel attribute
expect(auctionsTab?.getAttribute('panel')).toBe('auctions');
});
it('should render three tab panels', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabPanels = compiled.querySelectorAll('wa-tab-panel');
expect(tabPanels.length).toBe(3);
// NEW: Verify each panel has correct name attribute
const names = Array.from(tabPanels).map(p => p.getAttribute('name'));
expect(names).toContain('listings');
expect(names).toContain('fills');
expect(names).toContain('auctions');
});
it('should display empty state for listings', () => {
const compiled = fixture.nativeElement as HTMLElement;
const listingsPanel = compiled.querySelector('wa-tab-panel[name="listings"]');
expect(listingsPanel?.textContent).toContain('You have no active listings');
// NEW: Verify listings panel has callout
const callout = listingsPanel?.querySelector('wa-callout');
expect(callout).toBeTruthy();
});
it('should display empty state for fills', () => {
const compiled = fixture.nativeElement as HTMLElement;
const fillsPanel = compiled.querySelector('wa-tab-panel[name="fills"]');
expect(fillsPanel?.textContent).toContain('You have no filled orders');
// NEW: Verify fills panel has empty state icon
const icon = fillsPanel?.querySelector('wa-icon');
expect(icon).toBeTruthy();
});
it('should display empty state for auctions', () => {
const compiled = fixture.nativeElement as HTMLElement;
const auctionsPanel = compiled.querySelector('wa-tab-panel[name="auctions"]');
expect(auctionsPanel?.textContent).toContain('No active auctions found');
// NEW: Verify auctions panel has refresh button
const button = auctionsPanel?.querySelector('wa-button');
expect(button).toBeTruthy();
});
});

View file

@ -0,0 +1,91 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'app-exchange-page',
imports: [TranslateModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="exchange">
<wa-tab-group>
<wa-tab slot="nav" panel="listings">{{ 'exchange.listings' | translate }}</wa-tab>
<wa-tab slot="nav" panel="fills">{{ 'exchange.fills' | translate }}</wa-tab>
<wa-tab slot="nav" panel="auctions">{{ 'exchange.auctions' | translate }}</wa-tab>
<wa-tab-panel name="listings">
<div class="exchange__content">
<div class="exchange__header">
<h3 style="margin: 0;">{{ 'exchange.yourListings' | translate }}</h3>
<wa-button size="small" variant="primary">
<wa-icon slot="prefix" name="fa-solid fa-plus"></wa-icon>
{{ 'exchange.createListing' | translate }}
</wa-button>
</div>
<wa-callout variant="neutral">
<div class="exchange__empty">
<wa-icon name="fa-solid fa-store" style="font-size: 2.5rem; opacity: 0.3; margin-bottom: 1rem;"></wa-icon>
<p>{{ 'exchange.noActiveListings' | translate }}</p>
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--wa-color-neutral-600);">
{{ 'exchange.listDomainsInfo' | translate }}
</p>
</div>
</wa-callout>
</div>
</wa-tab-panel>
<wa-tab-panel name="fills">
<div class="exchange__content">
<div class="exchange__header">
<h3 style="margin: 0;">{{ 'exchange.yourFills' | translate }}</h3>
</div>
<wa-callout variant="neutral">
<div class="exchange__empty">
<wa-icon name="fa-solid fa-handshake" style="font-size: 2.5rem; opacity: 0.3; margin-bottom: 1rem;"></wa-icon>
<p>{{ 'exchange.noFilledOrders' | translate }}</p>
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--wa-color-neutral-600);">
{{ 'exchange.completedPurchasesInfo' | translate }}
</p>
</div>
</wa-callout>
</div>
</wa-tab-panel>
<wa-tab-panel name="auctions">
<div class="exchange__content">
<div class="exchange__header">
<h3 style="margin: 0;">{{ 'exchange.marketplaceAuctions' | translate }}</h3>
<wa-button size="small" variant="neutral">
<wa-icon slot="prefix" name="fa-solid fa-rotate"></wa-icon>
{{ 'exchange.refresh' | translate }}
</wa-button>
</div>
<wa-callout variant="neutral">
<div class="exchange__empty">
<wa-icon name="fa-solid fa-gavel" style="font-size: 2.5rem; opacity: 0.3; margin-bottom: 1rem;"></wa-icon>
<p>{{ 'exchange.noActiveAuctions' | translate }}</p>
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--wa-color-neutral-600);">
{{ 'exchange.browseAuctionsInfo' | translate }}
</p>
</div>
</wa-callout>
</div>
</wa-tab-panel>
</wa-tab-group>
</div>
`,
styles: [`
.exchange { }
.exchange__content { padding: 1.5rem 0; }
.exchange__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.exchange__empty { text-align: center; padding: 2rem 1rem; }
.exchange__empty p { margin: 0; color: var(--wa-color-neutral-700, #374151); }
`]
})
export class ExchangePage {}

View file

@ -0,0 +1,86 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomePage } from './home.page';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomePage],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
// NEW: Verify component is an instance of HomePage
expect(component instanceof HomePage).toBe(true);
});
it('should render account header with balance sections', () => {
const compiled = fixture.nativeElement as HTMLElement;
const header = compiled.querySelector('.account__header');
expect(header).toBeTruthy();
// NEW: Verify header has at least 2 sections (spendable and locked)
const sections = compiled.querySelectorAll('.account__header__section');
expect(sections.length).toBeGreaterThanOrEqual(2);
});
it('should display spendable balance section', () => {
const compiled = fixture.nativeElement as HTMLElement;
const spendableLabel = compiled.querySelector('.label');
expect(spendableLabel?.textContent).toContain('Spendable');
// NEW: Verify there's an amount display
const amount = compiled.querySelector('.amount');
expect(amount).toBeTruthy();
});
it('should render actionable cards grid', () => {
const compiled = fixture.nativeElement as HTMLElement;
const cardsGrid = compiled.querySelector('.account__cards');
expect(cardsGrid).toBeTruthy();
// NEW: Verify grid contains wa-card elements
const cards = cardsGrid?.querySelectorAll('wa-card');
expect(cards?.length).toBeGreaterThan(0);
});
it('should render six action cards', () => {
const compiled = fixture.nativeElement as HTMLElement;
const cards = compiled.querySelectorAll('wa-card');
expect(cards.length).toBe(6);
// NEW: Verify each card has an icon
const icons = compiled.querySelectorAll('.account__card__icon');
expect(icons.length).toBe(6);
});
it('should render transaction history section', () => {
const compiled = fixture.nativeElement as HTMLElement;
const transactions = compiled.querySelector('.account__transactions');
expect(transactions).toBeTruthy();
// NEW: Verify transactions section has a title
const title = transactions?.querySelector('.account__panel-title');
expect(title).toBeTruthy();
});
it('should display transaction history title', () => {
const compiled = fixture.nativeElement as HTMLElement;
const title = compiled.querySelector('.account__panel-title');
expect(title?.textContent).toContain('Transaction History');
// NEW: Verify title is not empty
expect(title?.textContent?.trim().length).toBeGreaterThan(0);
});
it('should show empty state for transactions', () => {
const compiled = fixture.nativeElement as HTMLElement;
const callout = compiled.querySelector('wa-callout');
expect(callout?.textContent).toContain('No transactions yet');
// NEW: Verify callout has neutral variant attribute
expect(callout?.getAttribute('variant')).toBe('neutral');
});
});

View file

@ -0,0 +1,139 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'app-home-page',
imports: [TranslateModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="account">
<!-- Balance Header -->
<div class="account__header">
<div class="account__header__section">
<span class="label">{{ 'home.spendable' | translate }}</span>
<p class="amount">0.00 {{ 'common.hns' | translate }}</p>
<span class="subtext">~$0.00 {{ 'common.usd' | translate }}</span>
</div>
<div class="account__header__section">
<span class="label">{{ 'home.locked' | translate }}</span>
<p class="amount">0.00 {{ 'common.hns' | translate }}</p>
<span class="subtext">{{ 'home.inBids' | translate }} (0 {{ 'home.bids' | translate }})</span>
</div>
</div>
<!-- Actionable Items Cards -->
<div class="account__cards">
<wa-card class="account__card">
<div slot="header" class="account__card__header">
<wa-icon name="fa-solid fa-eye" class="account__card__icon"></wa-icon>
<span>{{ 'home.revealable' | translate }}</span>
</div>
<div class="account__card__content">
<div class="account__card__amount">0.00 {{ 'common.hns' | translate }}</div>
<div class="account__card__detail">{{ 'home.bidsReadyToReveal' | translate: {count: 0} }}</div>
<div class="account__card__action">
<wa-button size="small" disabled>{{ 'home.revealAll' | translate }}</wa-button>
</div>
</div>
</wa-card>
<wa-card class="account__card">
<div slot="header" class="account__card__header">
<wa-icon name="fa-solid fa-gift" class="account__card__icon"></wa-icon>
<span>{{ 'home.redeemable' | translate }}</span>
</div>
<div class="account__card__content">
<div class="account__card__amount">0.00 {{ 'common.hns' | translate }}</div>
<div class="account__card__detail">{{ 'home.bidsReadyToRedeem' | translate: {count: 0} }}</div>
<div class="account__card__action">
<wa-button size="small" disabled>{{ 'home.redeemAll' | translate }}</wa-button>
</div>
</div>
</wa-card>
<wa-card class="account__card">
<div slot="header" class="account__card__header">
<wa-icon name="fa-solid fa-pen-to-square" class="account__card__icon"></wa-icon>
<span>{{ 'home.registerable' | translate }}</span>
</div>
<div class="account__card__content">
<div class="account__card__amount">0.00 {{ 'common.hns' | translate }}</div>
<div class="account__card__detail">{{ 'home.namesReadyToRegister' | translate: {count: 0} }}</div>
<div class="account__card__action">
<wa-button size="small" disabled>{{ 'home.registerAll' | translate }}</wa-button>
</div>
</div>
</wa-card>
<wa-card class="account__card">
<div slot="header" class="account__card__header">
<wa-icon name="fa-solid fa-clock-rotate-left" class="account__card__icon"></wa-icon>
<span>{{ 'home.renewable' | translate }}</span>
</div>
<div class="account__card__content">
<div class="account__card__detail">{{ 'home.domainsExpiringSoon' | translate: {count: 0} }}</div>
<div class="account__card__action">
<wa-button size="small" disabled>{{ 'home.renewAll' | translate }}</wa-button>
</div>
</div>
</wa-card>
<wa-card class="account__card">
<div slot="header" class="account__card__header">
<wa-icon name="fa-solid fa-arrow-right-arrow-left" class="account__card__icon"></wa-icon>
<span>{{ 'home.transferring' | translate }}</span>
</div>
<div class="account__card__content">
<div class="account__card__detail">{{ 'home.domainsInTransfer' | translate: {count: 0} }}</div>
</div>
</wa-card>
<wa-card class="account__card">
<div slot="header" class="account__card__header">
<wa-icon name="fa-solid fa-check" class="account__card__icon"></wa-icon>
<span>{{ 'home.finalizable' | translate }}</span>
</div>
<div class="account__card__content">
<div class="account__card__detail">{{ 'home.transfersReadyToFinalize' | translate: {count: 0} }}</div>
<div class="account__card__action">
<wa-button size="small" disabled>{{ 'home.finalizeAll' | translate }}</wa-button>
</div>
</div>
</wa-card>
</div>
<!-- Transaction History -->
<div class="account__transactions">
<div class="account__panel-title">{{ 'home.transactionHistory' | translate }}</div>
<wa-callout variant="neutral">
{{ 'home.noTransactions' | translate }}
</wa-callout>
</div>
</div>
`,
styles: [`
.account { display: flex; flex-direction: column; gap: 1.5rem; }
.account__header { display: flex; gap: 1rem; flex-wrap: wrap; padding: 1.5rem; background: var(--wa-color-neutral-50, #fafafa); border-radius: 0.5rem; border: 1px solid var(--wa-color-neutral-200, #e5e7eb); }
.account__header__section { display: flex; flex-direction: column; padding: 0 1rem; }
.account__header__section:not(:last-child) { border-right: 1px solid var(--wa-color-neutral-300, #d1d5db); }
.account__header__section .label { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; color: var(--wa-color-neutral-600, #4b5563); margin-bottom: 0.25rem; }
.account__header__section .amount { font-size: 1.875rem; font-weight: 700; color: var(--wa-color-neutral-900, #111827); margin: 0.25rem 0; }
.account__header__section .subtext { font-size: 0.875rem; color: var(--wa-color-neutral-500, #6b7280); }
.account__cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
.account__card { height: 100%; }
.account__card__header { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; padding: 1rem; }
.account__card__icon { font-size: 1.25rem; color: var(--wa-color-primary-600, #4f46e5); }
.account__card__content { padding: 0 1rem 1rem; }
.account__card__amount { font-size: 1.5rem; font-weight: 700; color: var(--wa-color-neutral-900, #111827); margin-bottom: 0.5rem; }
.account__card__detail { font-size: 0.875rem; color: var(--wa-color-neutral-600, #4b5563); margin-bottom: 0.75rem; }
.account__card__action { margin-top: 0.75rem; }
.account__transactions { }
.account__panel-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; }
`]
})
export class HomePage {}

View file

@ -0,0 +1,126 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-onboarding-page',
imports: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="onboarding">
<div class="onboarding__header">
<h2 style="margin: 0;">Welcome to Bob Wallet</h2>
<p style="margin: 0.5rem 0 0 0; color: var(--wa-color-neutral-600);">
Set up your wallet to start managing Handshake names
</p>
</div>
<wa-tab-group>
<wa-tab slot="nav" panel="create">Create New Wallet</wa-tab>
<wa-tab slot="nav" panel="import">Import Seed</wa-tab>
<wa-tab slot="nav" panel="ledger">Connect Ledger</wa-tab>
<wa-tab-panel name="create">
<div class="onboarding__content">
<wa-callout variant="info">
<strong>Important:</strong> Write down your seed phrase and store it in a secure location.
You will need it to recover your wallet.
</wa-callout>
<div class="seed-display">
<div class="seed-words">
<div class="seed-word">abandon</div>
<div class="seed-word">ability</div>
<div class="seed-word">able</div>
<div class="seed-word">about</div>
<div class="seed-word">above</div>
<div class="seed-word">absent</div>
<div class="seed-word">absorb</div>
<div class="seed-word">abstract</div>
<div class="seed-word">absurd</div>
<div class="seed-word">abuse</div>
<div class="seed-word">access</div>
<div class="seed-word">accident</div>
</div>
</div>
<div class="onboarding__actions">
<wa-button variant="primary">
<wa-icon slot="prefix" name="fa-solid fa-copy"></wa-icon>
Copy Seed Phrase
</wa-button>
<wa-button variant="neutral">
<wa-icon slot="prefix" name="fa-solid fa-check"></wa-icon>
I've Saved My Seed
</wa-button>
</div>
</div>
</wa-tab-panel>
<wa-tab-panel name="import">
<div class="onboarding__content">
<wa-callout variant="neutral">
Enter your 12 or 24 word seed phrase to restore an existing wallet.
</wa-callout>
<div style="margin-top: 1.5rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Seed Phrase</label>
<wa-input placeholder="Enter your seed phrase" style="width: 100%;">
<textarea slot="input" rows="4" style="resize: vertical; font-family: monospace;"></textarea>
</wa-input>
</div>
<div class="onboarding__actions">
<wa-button variant="primary">
<wa-icon slot="prefix" name="fa-solid fa-download"></wa-icon>
Import Wallet
</wa-button>
</div>
</div>
</wa-tab-panel>
<wa-tab-panel name="ledger">
<div class="onboarding__content">
<wa-callout variant="info">
Connect your Ledger hardware wallet to manage your Handshake names securely.
</wa-callout>
<div class="ledger-instructions">
<h4 style="margin: 1.5rem 0 1rem 0;">Instructions:</h4>
<ol style="margin: 0; padding-left: 1.5rem; color: var(--wa-color-neutral-700);">
<li style="margin-bottom: 0.5rem;">Connect your Ledger device via USB</li>
<li style="margin-bottom: 0.5rem;">Enter your PIN on the device</li>
<li style="margin-bottom: 0.5rem;">Open the Handshake app on your Ledger</li>
<li style="margin-bottom: 0.5rem;">Click "Connect" below</li>
</ol>
</div>
<div class="onboarding__actions">
<wa-button variant="primary">
<wa-icon slot="prefix" name="fa-solid fa-usb"></wa-icon>
Connect Ledger
</wa-button>
</div>
</div>
</wa-tab-panel>
</wa-tab-group>
</div>
`,
styles: [`
.onboarding { }
.onboarding__header { margin-bottom: 2rem; }
.onboarding__content { padding: 1.5rem 0; }
.seed-display { margin: 1.5rem 0; padding: 1.5rem; background: var(--wa-color-neutral-50, #fafafa); border: 2px solid var(--wa-color-neutral-200, #e5e7eb); border-radius: 0.5rem; }
.seed-words { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
.seed-word { padding: 0.75rem; background: white; border: 1px solid var(--wa-color-neutral-300, #d1d5db); border-radius: 0.375rem; font-family: monospace; font-size: 0.875rem; text-align: center; }
.onboarding__actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; }
.ledger-instructions { margin-top: 1rem; }
`]
})
export class OnboardingPage {}

View file

@ -0,0 +1,127 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
@Component({
selector: 'app-search-tld-page',
imports: [FormsModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="search-tld">
<div class="search-tld__header">
<wa-input placeholder="Search for a name..." style="flex: 1;">
<input slot="input" [(ngModel)]="query" (keyup.enter)="onSearch()" />
</wa-input>
<wa-button variant="primary" (click)="onSearch()" [disabled]="loading || !query">
<wa-icon slot="prefix" name="fa-solid fa-magnifying-glass"></wa-icon>
Search
</wa-button>
</div>
@if (loading) {
<div class="search-tld__content">
<wa-spinner></wa-spinner>
<p style="text-align: center; margin-top: 1rem;">Searching...</p>
</div>
}
@if (!loading && !result) {
<div class="search-tld__content">
<wa-callout variant="neutral">
<div style="text-align: center; padding: 1rem;">
<wa-icon name="fa-solid fa-search" style="font-size: 2.5rem; opacity: 0.3; margin-bottom: 1rem;"></wa-icon>
<p style="margin: 0;">Enter a name to search for availability and auction status.</p>
</div>
</wa-callout>
</div>
}
@if (!loading && result) {
<div class="search-tld__content">
<wa-card>
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0; font-size: 1.5rem;">{{result.name}}/</h3>
<wa-badge [attr.variant]="result.available ? 'success' : 'warning'">
{{result.available ? 'Available' : 'In Auction'}}
</wa-badge>
</div>
<div class="search-result">
<div class="search-result__section">
<div class="search-result__label">Status</div>
<div class="search-result__value">{{result.status}}</div>
</div>
@if (result.currentBid) {
<div class="search-result__section">
<div class="search-result__label">Current Bid</div>
<div class="search-result__value">{{result.currentBid}} HNS</div>
</div>
}
@if (result.blocksUntil) {
<div class="search-result__section">
<div class="search-result__label">{{result.blocksUntilLabel}}</div>
<div class="search-result__value">~{{result.blocksUntil}} blocks</div>
</div>
}
<div class="search-result__actions">
<wa-button variant="primary" [disabled]="!result.available">
<wa-icon slot="prefix" name="fa-solid fa-gavel"></wa-icon>
Place Bid
</wa-button>
<wa-button variant="neutral">
<wa-icon slot="prefix" name="fa-solid fa-eye"></wa-icon>
Watch
</wa-button>
</div>
</div>
</wa-card>
</div>
}
</div>
`,
styles: [`
.search-tld { display: flex; flex-direction: column; gap: 1.5rem; }
.search-tld__header { display: flex; gap: 1rem; align-items: center; }
.search-tld__content { }
.search-result { padding: 1rem 0; }
.search-result__section { display: flex; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid var(--wa-color-neutral-200, #e5e7eb); }
.search-result__section:last-of-type { border-bottom: none; }
.search-result__label { font-size: 0.875rem; font-weight: 600; color: var(--wa-color-neutral-600, #4b5563); }
.search-result__value { font-size: 0.875rem; color: var(--wa-color-neutral-900, #111827); font-weight: 500; }
.search-result__actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--wa-color-neutral-200, #e5e7eb); }
`]
})
export class SearchTldPage {
query = '';
loading = false;
result: any = null;
constructor(private router: Router) {}
async onSearch() {
if (!this.query.trim()) return;
this.loading = true;
this.result = null;
// Simulate API call
await new Promise(r => setTimeout(r, 800));
const name = this.query.trim().replace('/', '');
const available = Math.random() > 0.3;
this.result = {
name,
available,
status: available ? 'Available for bidding' : 'Auction in progress',
currentBid: available ? null : (Math.random() * 100).toFixed(2),
blocksUntil: available ? null : Math.floor(Math.random() * 5000),
blocksUntilLabel: available ? null : 'Blocks until reveal'
};
this.loading = false;
}
}

View file

@ -0,0 +1,95 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsPage } from './settings.page';
import { FileDialogService } from '../../services/file-dialog.service';
import { ClipboardService } from '../../services/clipboard.service';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('SettingsPage', () => {
let component: SettingsPage;
let fixture: ComponentFixture<SettingsPage>;
let fileDialogService: jasmine.SpyObj<FileDialogService>;
let clipboardService: jasmine.SpyObj<ClipboardService>;
beforeEach(async () => {
const fileDialogSpy = jasmine.createSpyObj('FileDialogService', ['pickDirectory', 'openFile', 'saveFile']);
const clipboardSpy = jasmine.createSpyObj('ClipboardService', ['copyText']);
await TestBed.configureTestingModule({
imports: [SettingsPage],
providers: [
{ provide: FileDialogService, useValue: fileDialogSpy },
{ provide: ClipboardService, useValue: clipboardSpy }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fileDialogService = TestBed.inject(FileDialogService) as jasmine.SpyObj<FileDialogService>;
clipboardService = TestBed.inject(ClipboardService) as jasmine.SpyObj<ClipboardService>;
fixture = TestBed.createComponent(SettingsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
// NEW: Verify component is an instance of SettingsPage
expect(component instanceof SettingsPage).toBe(true);
});
it('should initialize with default locale', () => {
expect(component.locale).toBe('en-US');
// NEW: Verify msg is initialized as empty string
expect(component.msg).toBe('');
});
it('should render settings tabs', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabGroup = compiled.querySelector('wa-tab-group');
expect(tabGroup).toBeTruthy();
// NEW: Verify tab group contains tab panels
const tabPanels = compiled.querySelectorAll('wa-tab-panel');
expect(tabPanels.length).toBeGreaterThan(0);
});
it('should render four tab panels', () => {
const compiled = fixture.nativeElement as HTMLElement;
const tabs = compiled.querySelectorAll('wa-tab');
expect(tabs.length).toBe(4);
// NEW: Verify corresponding tab panels exist
const tabPanels = compiled.querySelectorAll('wa-tab-panel');
expect(tabPanels.length).toBe(4);
});
it('should call pickDirectory when change directory button clicked', async () => {
fileDialogService.pickDirectory.and.returnValue(Promise.resolve({ path: '/test/path' }));
await component.pickDir();
expect(fileDialogService.pickDirectory).toHaveBeenCalled();
// NEW: Verify pickedPath is updated
expect(component.pickedPath).toBe('/test/path');
});
it('should call saveFile when export backup button clicked', async () => {
fileDialogService.saveFile.and.returnValue(Promise.resolve({ name: 'settings.json' } as any));
await component.saveFile();
expect(fileDialogService.saveFile).toHaveBeenCalled();
// NEW: Verify pickedPath is updated with filename
expect(component.pickedPath).toBe('settings.json');
});
it('should update message after saving locale', () => {
component.saveLocale();
expect(component.msg).toContain('Saved locale');
// NEW: Verify message includes the locale value
expect(component.msg).toContain('en-US');
});
it('should copy locale to clipboard', async () => {
clipboardService.copyText.and.returnValue(Promise.resolve(true));
await component.copyLocale();
expect(clipboardService.copyText).toHaveBeenCalledWith(component.locale);
expect(component.msg).toContain('copied to clipboard');
// NEW: Verify clipboard was called exactly once
expect(clipboardService.copyText).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,168 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FileDialogService } from '../../services/file-dialog.service';
import { ClipboardService } from '../../services/clipboard.service';
@Component({
selector: 'app-settings-page',
imports: [FormsModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="settings">
<wa-tab-group>
<wa-tab slot="nav" panel="general">General</wa-tab>
<wa-tab slot="nav" panel="wallet">Wallet</wa-tab>
<wa-tab slot="nav" panel="connection">Connection</wa-tab>
<wa-tab slot="nav" panel="advanced">Advanced</wa-tab>
<wa-tab-panel name="general">
<div class="settings__section">
<h3 class="settings__section-title">Language</h3>
<wa-select value="en-US" style="max-width: 300px;">
<wa-option value="en-US">English (US)</wa-option>
<wa-option value="zh-CN"> ()</wa-option>
<wa-option value="es-ES">Español</wa-option>
</wa-select>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Block Explorer</h3>
<wa-select value="hnsnetwork" style="max-width: 300px;">
<wa-option value="hnsnetwork">HNS Network</wa-option>
<wa-option value="niami">Niami</wa-option>
<wa-option value="hnscan">HNScan</wa-option>
</wa-select>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Theme</h3>
<wa-select value="light" style="max-width: 300px;">
<wa-option value="light">Light</wa-option>
<wa-option value="dark">Dark</wa-option>
<wa-option value="system">System</wa-option>
</wa-select>
</div>
</wa-tab-panel>
<wa-tab-panel name="wallet">
<div class="settings__section">
<h3 class="settings__section-title">Wallet Directory</h3>
<p class="settings__description">Location where wallet data is stored</p>
<wa-input value="~/.bob-wallet" readonly style="max-width: 500px;">
<input slot="input" />
</wa-input>
<div style="margin-top: 0.75rem;">
<wa-button size="small" (click)="pickDir()">Change Directory</wa-button>
</div>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Backup</h3>
<p class="settings__description">Export wallet seed phrase and settings</p>
<wa-button size="small" (click)="saveFile()">
<wa-icon slot="prefix" name="fa-solid fa-download"></wa-icon>
Export Backup
</wa-button>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Rescan Blockchain</h3>
<p class="settings__description">Re-scan the blockchain for transactions</p>
<wa-button size="small" variant="neutral">Rescan</wa-button>
</div>
</wa-tab-panel>
<wa-tab-panel name="connection">
<div class="settings__section">
<h3 class="settings__section-title">Connection Type</h3>
<wa-select value="full-node" style="max-width: 300px;">
<wa-option value="full-node">Full Node</wa-option>
<wa-option value="spv">SPV (Light)</wa-option>
<wa-option value="custom">Custom RPC</wa-option>
</wa-select>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Network</h3>
<wa-select value="main" style="max-width: 300px;">
<wa-option value="main">Mainnet</wa-option>
<wa-option value="testnet">Testnet</wa-option>
<wa-option value="regtest">Regtest</wa-option>
<wa-option value="simnet">Simnet</wa-option>
</wa-select>
</div>
</wa-tab-panel>
<wa-tab-panel name="advanced">
<div class="settings__section">
<h3 class="settings__section-title">API Key</h3>
<p class="settings__description">Node API authentication key</p>
<wa-input type="password" style="max-width: 400px;">
<input slot="input" type="password" />
</wa-input>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Analytics</h3>
<wa-checkbox checked>Share anonymous usage data to improve Bob</wa-checkbox>
</div>
<div class="settings__section">
<h3 class="settings__section-title">Developer Options</h3>
<wa-button size="small" variant="neutral">
<wa-icon slot="prefix" name="fa-solid fa-bug"></wa-icon>
Open Debug Console
</wa-button>
</div>
</wa-tab-panel>
</wa-tab-group>
</div>
`,
styles: [`
.settings { }
.settings__section { padding: 1.5rem 0; border-bottom: 1px solid var(--wa-color-neutral-200, #e5e7eb); }
.settings__section:last-child { border-bottom: none; }
.settings__section-title { margin: 0 0 0.5rem 0; font-size: 1rem; font-weight: 600; }
.settings__description { margin: 0 0 0.75rem 0; font-size: 0.875rem; color: var(--wa-color-neutral-600, #4b5563); }
`]
})
export class SettingsPage {
private fileDialog = inject(FileDialogService);
private clipboard = inject(ClipboardService);
locale = 'en-US';
msg = '';
pickedPath = '';
saveLocale() {
// TODO: connect to Setting.setLocale via IPC when available
this.msg = `Saved locale: ${this.locale}`;
setTimeout(() => (this.msg = ''), 1500);
}
async copyLocale() {
await this.clipboard.copyText(this.locale);
this.msg = 'Locale copied to clipboard';
setTimeout(() => (this.msg = ''), 1500);
}
async pickDir() {
const res = await this.fileDialog.pickDirectory();
this.pickedPath = res?.path || res?.name || '';
}
async pickFile() {
const res = await this.fileDialog.openFile({ multiple: false, accept: ['application/json'] });
this.pickedPath = res?.[0]?.name || '';
}
async saveFile() {
const data = new Blob([JSON.stringify({ locale: this.locale }, null, 2)], { type: 'application/json' });
const file = await this.fileDialog.saveFile({ suggestedName: 'settings.json', blob: data });
this.pickedPath = file?.name || '';
}
}

View file

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ClipboardService {
async copyText(text: string): Promise<boolean> {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) {
// fall back
}
// Fallback using a hidden textarea
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
return true;
} catch (e) {
return false;
} finally {
document.body.removeChild(ta);
}
}
}

View file

@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
// WAILS3 INTEGRATION:
// This service currently uses web-standard File System Access API.
// For Wails3, replace with Go service methods calling:
// - application.OpenFileDialog().PromptForSingleSelection()
// - application.SaveFileDialog().SetFilename().PromptForSelection()
// See WAILS3_INTEGRATION.md for complete examples.
export interface OpenFileOptions {
multiple?: boolean;
accept?: string[]; // e.g., ["application/json", "text/plain"]
}
export interface SaveFileOptions {
suggestedName?: string;
types?: { description?: string; accept?: Record<string, string[]> }[];
blob: Blob;
}
@Injectable({ providedIn: 'root' })
export class FileDialogService {
// Directory picker using File System Access API when available
async pickDirectory(): Promise<any | null> {
const nav: any = window.navigator;
if ((window as any).showDirectoryPicker) {
try {
// @ts-ignore
const handle: any = await (window as any).showDirectoryPicker({ mode: 'readwrite' });
return handle;
} catch (e) {
return null;
}
}
// Fallback: not supported in all browsers; inform the user
alert('Directory picker is not supported in this browser.');
return null;
}
// Open file(s) with <input type="file"> fallback if FS Access API not used
async openFile(opts: OpenFileOptions = {}): Promise<File[] | null> {
// Always supported fallback
return new Promise<File[] | null>((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = !!opts.multiple;
if (opts.accept && opts.accept.length) {
input.accept = opts.accept.join(',');
}
input.onchange = () => {
const files = input.files ? Array.from(input.files) : null;
resolve(files);
};
input.click();
});
}
// Save file using File System Access API if available, otherwise trigger a download
async saveFile(opts: SaveFileOptions): Promise<File | null> {
if ((window as any).showSaveFilePicker) {
try {
// @ts-ignore
const handle = await (window as any).showSaveFilePicker({
suggestedName: opts.suggestedName,
types: opts.types
});
const writable = await handle.createWritable();
await writable.write(opts.blob);
await writable.close();
return { name: handle.name } as any;
} catch (e) {
return null;
}
}
// Fallback: download
const url = URL.createObjectURL(opts.blob);
const a = document.createElement('a');
a.href = url;
a.download = opts.suggestedName || 'download';
a.click();
URL.revokeObjectURL(url);
return { name: opts.suggestedName || 'download' } as any;
}
}

View file

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class HardwareWalletService {
// Placeholder for WebHID/WebUSB detection
get isWebHIDAvailable() {
return 'hid' in navigator;
}
get isWebUSBAvailable() {
return 'usb' in navigator;
}
async connectLedger(): Promise<void> {
// In a real implementation, prompt for a specific HID/USB device
// and establish transport (e.g., via @ledgerhq/hw-transport-webhid).
// This is a stub to document the integration point.
throw new Error('HardwareWalletService.connectLedger is not implemented in the web build.');
}
async getAppVersion(): Promise<string> {
// Should query the connected device/app for version information
throw new Error('HardwareWalletService.getAppVersion is not implemented in the web build.');
}
async disconnect(): Promise<void> {
// Close transport/session to the device
throw new Error('HardwareWalletService.disconnect is not implemented in the web build.');
}
}

View file

@ -0,0 +1,233 @@
// IPC Stub classes mapping old Electron IPC services and methods.
// These stubs let the web build compile and run without native IPC.
// Each method throws a NotImplementedError to highlight what needs
// to be replaced in a native wrapper or future web-compatible API.
//
// WAILS3 INTEGRATION:
// These stubs will be replaced by Wails3 auto-generated bindings.
// See WAILS3_INTEGRATION.md for complete migration guide.
//
// Pattern:
// 1. Create Go service structs with exported methods (e.g., NodeService, WalletService)
// 2. Register services in Wails3 main.go: application.NewService(&NodeService{})
// 3. Run `wails3 generate bindings` to create TypeScript bindings
// 4. Import generated bindings: import { GetInfo } from '../bindings/.../nodeservice'
// 5. Replace stub calls with binding calls: await GetInfo() instead of IPC.Node.getInfo()
//
// Each service below maps 1:1 to a Go service struct that will be created.
export class NotImplementedError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotImplementedError';
}
}
function notImplemented(service: string, method: string): never {
throw new NotImplementedError(`IPC ${service}.${method} is not implemented in the web build.`);
}
function makeIpcStub<T extends Record<string, any>>(service: string, methods: string[]): T {
const obj: Record<string, any> = {};
for (const m of methods) {
obj[m] = (..._args: any[]) => notImplemented(service, m);
}
return obj as T;
}
// Services and their methods as defined in old/app/background/**/client.js
export const Node = makeIpcStub('Node', [
'start',
'stop',
'reset',
'generateToAddress',
'getAPIKey',
'getNoDns',
'getSpvMode',
'getInfo',
'getNameInfo',
'getTXByAddresses',
'getNameByHash',
'getBlockByHeight',
'getTx',
'broadcastRawTx',
'sendRawAirdrop',
'getFees',
'getAverageBlockTime',
'getMTP',
'getCoin',
'verifyMessageWithName',
'setNodeDir',
'setAPIKey',
'setNoDns',
'setSpvMode',
'getDir',
'getHNSPrice',
'testCustomRPCClient',
'getDNSSECProof',
'sendRawClaim',
]);
export const Wallet = makeIpcStub('Wallet', [
'start',
'getAPIKey',
'setAPIKey',
'getWalletInfo',
'getAccountInfo',
'getCoin',
'getTX',
'getNames',
'createNewWallet',
'importSeed',
'generateReceivingAddress',
'getAuctionInfo',
'getTransactionHistory',
'getPendingTransactions',
'getBids',
'getBlind',
'getMasterHDKey',
'hasAddress',
'setPassphrase',
'revealSeed',
'estimateTxFee',
'estimateMaxSend',
'removeWalletById',
'updateAccountDepth',
'findNonce',
'findNonceCancel',
'encryptWallet',
'backup',
'rescan',
'deepClean',
'reset',
'sendOpen',
'sendBid',
'sendRegister',
'sendUpdate',
'sendReveal',
'sendRedeem',
'sendRenewal',
'sendRevealAll',
'sendRedeemAll',
'sendRegisterAll',
'signMessageWithName',
'transferMany',
'finalizeAll',
'finalizeMany',
'renewAll',
'renewMany',
'sendTransfer',
'cancelTransfer',
'finalizeTransfer',
'finalizeWithPayment',
'claimPaidTransfer',
'revokeName',
'send',
'lock',
'unlock',
'isLocked',
'addSharedKey',
'removeSharedKey',
'getNonce',
'importNonce',
'zap',
'importName',
'rpcGetWalletInfo',
'loadTransaction',
'listWallets',
'getStats',
'isReady',
'createClaim',
'sendClaim',
]);
export const Setting = makeIpcStub('Setting', [
'getExplorer',
'setExplorer',
'getLocale',
'setLocale',
'getCustomLocale',
'setCustomLocale',
'getLatestRelease',
]);
export const Ledger = makeIpcStub('Ledger', [
'getXPub',
'getAppVersion',
]);
export const DB = makeIpcStub('DB', [
'open',
'close',
'put',
'get',
'del',
'getUserDir',
]);
export const Analytics = makeIpcStub('Analytics', [
'setOptIn',
'getOptIn',
'track',
'screenView',
]);
export const Connections = makeIpcStub('Connections', [
'getConnection',
'setConnection',
'setConnectionType',
'getCustomRPC',
]);
export const Shakedex = makeIpcStub('Shakedex', [
'fulfillSwap',
'getFulfillments',
'finalizeSwap',
'transferLock',
'transferCancel',
'getListings',
'finalizeLock',
'finalizeCancel',
'launchAuction',
'downloadProofs',
'restoreOneListing',
'restoreOneFill',
'getExchangeAuctions',
'listAuction',
'getFeeInfo',
'getBestBid',
]);
export const Claim = makeIpcStub('Claim', [
'airdropGenerateProofs',
]);
export const Logger = makeIpcStub('Logger', [
'info',
'warn',
'error',
'log',
'download',
]);
export const Hip2 = makeIpcStub('Hip2', [
'getPort',
'setPort',
'fetchAddress',
'setServers',
]);
// Aggregate facade to import from components/services if needed
export const IPC = {
Node,
Wallet,
Setting,
Ledger,
DB,
Analytics,
Connections,
Shakedex,
Claim,
Logger,
Hip2,
};

View file

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class NotificationsService {
async requestPermission(): Promise<NotificationPermission> {
if (!('Notification' in window)) return 'denied';
if (Notification.permission === 'default') {
try {
return await Notification.requestPermission();
} catch {
return Notification.permission;
}
}
return Notification.permission;
}
async show(title: string, options?: NotificationOptions): Promise<void> {
if (!('Notification' in window)) return;
const perm = await this.requestPermission();
if (perm === 'granted') {
new Notification(title, options);
}
}
}

View file

@ -0,0 +1,6 @@
import { InjectionToken } from '@angular/core';
export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage
});

View file

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class StorageService {
private prefix = 'lthnDNS:';
setItem<T = unknown>(key: string, value: T): void {
try {
localStorage.setItem(this.prefix + key, JSON.stringify(value));
} catch (e) {
// ignore quota or unsupported errors
}
}
getItem<T = unknown>(key: string, fallback: T | null = null): T | null {
const raw = localStorage.getItem(this.prefix + key);
if (!raw) return fallback;
try {
return JSON.parse(raw) as T;
} catch {
return fallback;
}
}
removeItem(key: string): void {
localStorage.removeItem(this.prefix + key);
}
clearAll(): void {
Object.keys(localStorage)
.filter(k => k.startsWith(this.prefix))
.forEach(k => localStorage.removeItem(k));
}
}

View file

@ -0,0 +1,65 @@
.nga-form-check-input {
--bs-form-check-bg: var(--bs-body-bg);
width: 1em;
height: 1em;
margin-top: 0.25em;
vertical-align: top;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: var(--bs-form-check-bg);
background-image: var(--bs-form-check-bg-image);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
border: var(--bs-border-width) solid var(--bs-border-color);
}
.nga-form-check-input[type="checkbox"] {
border-radius: 0.25em;
}
.nga-form-check-input[type="radio"] {
border-radius: 50%;
}
.nga-form-check-input:active {
filter: brightness(90%);
}
.nga-form-check-input:focus {
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.nga-form-check-input:checked {
background-color: green;
border-color: green;
}
.nga-form-check-input:checked[type="checkbox"] {
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
}
.nga-form-check-input:checked[type="radio"] {
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e");
}
.nga-form-check-input[type="checkbox"]:indeterminate {
background-color: red;
border-color: red;
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
}
.nga-form-check-input:disabled {
pointer-events: none;
filter: none;
opacity: 0.5;
}
.nga-form-check-input[disabled]~.form-check-label,
.form-check-input:disabled~.form-check-label {
cursor: default;
opacity: 0.5;
}

View file

@ -0,0 +1,8 @@
<input #checkbox
class="nga-form-check-input"
type="checkbox"
(click)="onSelect()"
id="checkbox"
value=""
[(ngModel)]="valueTmp"
/>

View file

@ -0,0 +1,115 @@
import { CommonModule } from '@angular/common';
import {
Component, EventEmitter, Output, forwardRef,
ElementRef, Renderer2, ViewChild
} from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-checkbox',
imports: [
CommonModule,
FormsModule,
],
templateUrl: './checkbox.component.html',
styleUrls: ['./checkbox.component.css'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckboxComponent),
multi: true
}]
})
export class CheckboxComponent {
private innerValueTmp: any = '';
private onTouchedCallback = (): void => {
// Callback function intentionally left blank.
};
private onChangeCallback = (_value: unknown): void => {
// Callback function intentionally left blank.
};
@ViewChild('checkbox', { static: false }) checkbox!: ElementRef;
@Output() buttonclick: EventEmitter<number> = new EventEmitter<number>();
valueCheckbox: any;
indeterminate: any;
checked: any;
constructor(
private renderer: Renderer2) {
this.valueCheckbox = null;
}
onSelect() {
let value = this.checkbox.nativeElement.value;
switch (value) {
case "":
this.checked = true;
this.indeterminate = false;
value = "true";
this.valueCheckbox = true;
break;
case "true":
this.checked = false;
this.indeterminate = true;
value = "false";
this.valueCheckbox = false;
break;
case "false":
this.checked = null;
this.indeterminate = false;
value = "";
this.valueCheckbox = "";
break;
}
this.innerValueTmp = 4;
this.renderer.setAttribute(this.checkbox.nativeElement, 'value', value);
this.renderer.setProperty(this.checkbox.nativeElement, 'checked', this.checked);
this.renderer.setProperty(this.checkbox.nativeElement, 'indeterminate', this.indeterminate);
}
onClickButton() {
const value = this.checkbox.nativeElement.getAttribute('value');
// const indeterminate = this.checkbox.nativeElement.getProperty('indeterminate');
this.buttonclick.emit(value);
}
get valueTmp(): any {
return this.innerValueTmp;
};
set valueTmp(value: any) {
if (value !== this.innerValueTmp) {
if (this.checked && !this.indeterminate) { value = true; }
if (!this.checked && this.indeterminate) { value = false; }
if ((this.checked === null) && !this.indeterminate) { value = null; }
this.innerValueTmp = value;
this.onChangeCallback(value);
}
}
onBlur() {
this.onTouchedCallback();
}
writeValue(valueTmp: any) {
if (valueTmp !== this.innerValueTmp) {
this.innerValueTmp = valueTmp;
}
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}

View file

@ -0,0 +1,79 @@
.nga-footer {
background-color: #212121;
color: white;
}
.nga-footer a {
color: white;
text-decoration: none
}
.nga-footer a:hover,
.nga-footer a:focus {
color: white;
text-decoration: underline;
}
.nga-footer .nga-hint {
background-color: #1976d2;
}
.nga-footer .nga-hint:hover {
opacity: 0.8;
}
.nga-btn-social {
position: relative;
z-index: 1;
display: inline-block;
padding: 0;
margin: 10px;
overflow: hidden;
vertical-align: middle;
cursor: pointer;
border-radius: 50%;
-webkit-box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
-webkit-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
width: 47px;
height: 47px
}
.nga-btn-social i {
font-size: 1.25rem;
line-height: 47px
}
.nga-btn-social i {
display: inline-block;
width: inherit;
color: white;
text-align: center
}
.nga-btn-social:hover {
-webkit-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)
}
.nga-btn-social i:hover {
color: black;
}
.nga-btn-github {
background-color: #333;
}
.nga-btn-gitlab {
background-color: #ff4500;
}
.nga-btn-linkedin {
background-color: #0082ca;
}
.nga-btn-twitter {
background-color: #55acee;
}

View file

@ -0,0 +1,62 @@
<footer class="nga-footer">
<div class="nga-hint">
<div class="container">
<div class="row p-4"></div>
</div>
</div>
<div class="container py-5 text-center text-lg-start">
<div class="row">
<div class="col-12 col-lg-5 mb-3">
<h2 class="h5">
<img class="mb-1 me-1" [src]="'./assets/params/images/logo/' + appInfo.logo + '-logo.png'"
[srcset]="'./assets/params/images/logo/' + appInfo.logo + '-logo.png, ./assets/params/images/logo/' + appInfo.logo + '-logo@2x.png 2x'"
width="25" height="25" [alt]="'Logo ' + appInfo.name">
{{ appInfo.name }}
</h2>
<hr class="text-white mb-4 mt-0 d-inline-block" style="width: 120px;">
<p>Web Application : Angular 17, Bootstrap 5</p>
<p>Routing, Lazy Loading, SSR, PWA, SEO</p>
<div>
<a type="button" class="nga-btn-social nga-btn-linkedin"
[href]="'https://www.linkedin.com/in/' + appInfo.linkedinnetwork" [attr.aria-label]="'Linkedin ' + appInfo.name">
<i class="fab fa-linkedin-in"></i>
</a>
<a type="button" class="nga-btn-social nga-btn-twitter" [href]="'https://x.com/' + appInfo.xnetwork"
[attr.aria-label]="'Twitter ' + appInfo.name">
<i class="fab fa-twitter"></i>
</a>
<a type="button" class="nga-btn-social nga-btn-github" [href]="'https://github.com/' + appInfo.network"
[attr.aria-label]="'Github ' + appInfo.name">
<i class="fab fa-github"></i>
</a>
<a type="button" class="nga-btn-social nga-btn-gitlab" [href]="'https://gitlab.com/' + appInfo.network"
[attr.aria-label]="'Gitlab ' + appInfo.name">
<i class="fab fa-gitlab"></i>
</a>
</div>
</div>
<div class="col-6 col-lg-3 mb-3">
<h2 class="h5">Tools</h2>
<hr class="text-white mt-0 d-inline-block" style="width: 70px;">
<ul class="list-unstyled">
<li class="mb-2"><a href="https://angular.io/">Angular</a></li>
<li class="mb-2"><a href="https://getbootstrap.com/">Bootstrap</a></li>
<li class="mb-2"><a href="https://fontawesome.com/">Font Awesome</a></li>
</ul>
</div>
<div class="col-6 col-lg-3 mb-3">
<h2 class="h5">Learn</h2>
<hr class="text-white mt-0 d-inline-block" style="width: 70px;">
<ul class="list-unstyled">
<li class="mb-2"><a [href]="'https://' + appInfo.website + '/tutorials'">Tutorials</a></li>
<li class="mb-2"><a [href]="'https://' + appInfo.website + '/about'">About</a></li>
</ul>
</div>
</div>
</div>
<div class="py-3 text-center" style="background-color: black;">
<div class="container">
<a [href]="'https://' + appInfo.website">{{ appInfo.website }}</a>
</div>
</div>
</footer>

View file

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { environment } from '../../../../environments/environment';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.css']
})
export class FooterComponent {
appInfo = environment.appInfo;
}

View file

@ -0,0 +1,38 @@
.nga-nav-link {
color: white;
border-top: 1px solid #09238d;
border-bottom: 1px solid #09238d;
font-weight: 500;
}
.nga-nav-link:hover {
color: yellow;
border-top: 1px solid yellow;
border-bottom: 1px solid yellow;
}
.nga-navbar {
-webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 11px 10px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 11px 10px 0 rgba(0, 0, 0, 0.12);
background-color: #09238d;
}
.nga-logo {
font-weight: 500;
}
.nga-logo:hover {
color: rgba(255, 255, 255, 0.75);
}
.nga-btn-navbar {
--bs-btn-color: #fff;
--bs-btn-bg: #1976d2;
--bs-btn-border-color: #1976d2;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #0b5ed7;
--bs-btn-hover-border-color: #0a58ca;
}

View file

@ -0,0 +1,11 @@
<header class="nga-navbar" style="position: sticky; top: 0; z-index: 1000;">
<div class="topbar" style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--sl-color-neutral-200, #e5e7eb); background: var(--sl-color-neutral-0, #fff);">
<wa-button variant="text" size="small" (click)="onMenuClick()" aria-label="Toggle navigation">
<i class="fas fa-bars"></i>
</wa-button>
<a routerLink="/" class="brand" style="font-weight: 600; text-decoration: none; color: inherit;">Core Admin</a>
<div style="margin-left: auto;"></div>
</div>
</header>

View file

@ -0,0 +1,18 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, Output} from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-header',
imports: [CommonModule, RouterLink],
templateUrl: './header.component.html',
styleUrls: ['./header.component.css'],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HeaderComponent {
@Output() menuToggle = new EventEmitter<void>();
onMenuClick() {
this.menuToggle.emit();
}
}

View file

@ -0,0 +1,4 @@
export enum SortDirection {
ASC = 'asc',
DESC = 'desc'
}

View file

@ -0,0 +1,30 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'dateFormat'
})
export class DateFormatPipe implements PipeTransform {
transform(value: string | null | undefined): string {
if (!value) {
return '';
}
const regex = /^([0-2][0-9]|3[0-1])\/([0][1-9]|1[0-2])\/[0-9]{4} ([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
if (regex.test(value)) {
const [day, month, year] = value.split(/[/ ]/);
return `${day}/${month}/${year}`;
}
const date = new Date(value);
if (!isNaN(date.getTime())) {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
return '';
}
}

View file

@ -0,0 +1,29 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'dateHourFormat'
})
export class DateHourFormatPipe implements PipeTransform {
transform(value: string | null | undefined): string {
if (!value) {
return '';
}
const regex = /^([0-2][0-9]|3[0-1])\/([0][1-9]|1[0-2])\/[0-9]{4} ([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
if (regex.test(value)) {
return value;
}
const date = new Date(value);
if (!isNaN(date.getTime())) {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
}
return '';
}
}

View file

@ -0,0 +1,157 @@
import { PaginationService } from './pagination.service';
import { Pagination } from './pagination';
describe('PaginationService', () => {
let service: PaginationService;
beforeEach(() => {
service = new PaginationService();
});
it('should initialize a Pagination object correctly', () => {
// Arrange
const perPage = 5;
// Act
const pagination: Pagination = service.initializePagination(perPage);
// Assert
expect(pagination).toEqual({
totalItems: 0,
currentPage: 1,
perPage,
totalPages: 0,
startPage: 1,
endPage: 1,
pages: [],
pageBrowser: false,
useful: false
});
});
it('should handle the case where currentPage is greater than the total number of pages', () => {
// Arrange
const input = {
totalItems: 10,
currentPage: 5,
perPage: 5,
totalPages: 2,
startPage: 1,
endPage: 2,
pages: [1, 2],
pageBrowser: true,
useful: true
};
// Act
const pagination = service.getPagination(input);
// Assert
expect(pagination.currentPage).toBe(1);
expect(pagination.totalPages).toBe(2);
});
it('should handle a small number of pages (≤ 7 pages) correctly', () => {
// Arrange
const input = {
totalItems: 25,
currentPage: 3,
perPage: 5,
totalPages: 5,
startPage: 1,
endPage: 5,
pages: [1, 2, 3, 4, 5],
pageBrowser: true,
useful: true
};
// Act
const pagination = service.getPagination(input);
// Assert
expect(pagination.startPage).toBe(1);
expect(pagination.endPage).toBe(5);
expect(pagination.pages).toEqual([1, 2, 3, 4, 5]);
});
it('should handle the first pages with many total pages', () => {
// Arrange
const input = {
totalItems: 100,
currentPage: 3,
perPage: 5,
totalPages: 20,
startPage: 1,
endPage: 7,
pages: [1, 2, 3, 4, 5, 6, 7],
pageBrowser: true,
useful: true
};
// Act
const pagination = service.getPagination(input);
// Assert
expect(pagination.startPage).toBe(1);
expect(pagination.endPage).toBe(7);
expect(pagination.pages.length).toBe(7);
});
it('should handle the last pages with many total pages', () => {
// Arrange
const input = {
totalItems: 100,
currentPage: 18,
perPage: 5,
totalPages: 20,
startPage: 14,
endPage: 20,
pages: [14, 15, 16, 17, 18, 19, 20],
pageBrowser: true,
useful: true
};
// Act
const pagination = service.getPagination(input);
// Assert
expect(pagination.startPage).toBe(14);
expect(pagination.endPage).toBe(20);
expect(pagination.pages.length).toBe(7);
});
it('should handle middle pages with many total pages', () => {
// Arrange
const input = {
totalItems: 100,
currentPage: 10,
perPage: 5,
totalPages: 20,
startPage: 8,
endPage: 14,
pages: [8, 9, 10, 11, 12, 13, 14],
pageBrowser: true,
useful: true
};
// Act
const pagination = service.getPagination(input);
// Assert
expect(pagination.startPage).toBe(8);
expect(pagination.endPage).toBe(14);
expect(pagination.pages.length).toBe(7);
});
it('should generate a correct array of numbers with range()', () => {
// Arrange
const start = 1;
const end = 5;
// Act
const result = (service as any).range(start, end);
// Assert
expect(result).toEqual([1, 2, 3, 4]);
});
});

View file

@ -0,0 +1,72 @@
import { Injectable } from "@angular/core";
import { Pagination } from './pagination';
@Injectable()
export class PaginationService {
private readonly MAX_PAGES_DISPLAYED = 7;
private readonly STARTING_PAGE = 1;
range(start: number, end: number): number[] {
const length = end - start;
return Array.from({ length }, (__, index) => start + index);
}
getPagination(pagination: Pagination): Pagination {
const { totalItems, perPage } = pagination;
let currentPage = pagination.currentPage;
const totalPages = Math.ceil(totalItems / perPage);
if (currentPage > totalPages) {
currentPage = this.STARTING_PAGE;
}
const { startPage, endPage } = this.calculatePageRange(currentPage, totalPages);
const pages = this.range(startPage, endPage + 1);
return {
totalItems,
currentPage,
perPage,
totalPages,
startPage,
endPage,
pages,
pageBrowser: totalPages > 0,
useful: totalPages > 1
};
}
private calculatePageRange(currentPage: number, totalPages: number): { startPage: number, endPage: number } {
if (totalPages <= this.MAX_PAGES_DISPLAYED) {
return { startPage: this.STARTING_PAGE, endPage: totalPages };
}
if (currentPage <= this.MAX_PAGES_DISPLAYED - 1) {
return { startPage: this.STARTING_PAGE, endPage: this.MAX_PAGES_DISPLAYED };
}
if (currentPage + 4 >= totalPages) {
return { startPage: totalPages - (this.MAX_PAGES_DISPLAYED - 1), endPage: totalPages };
}
return { startPage: currentPage - 2, endPage: currentPage + 4 };
}
initializePagination(perPage: number): Pagination {
return {
totalItems: 0,
currentPage: this.STARTING_PAGE,
perPage,
totalPages: 0,
startPage: this.STARTING_PAGE,
endPage: this.STARTING_PAGE,
pages: [],
pageBrowser: false,
useful: false
};
}
}

View file

@ -0,0 +1,11 @@
export interface Pagination {
totalItems: number;
currentPage: number,
perPage: number,
totalPages: number,
startPage: number,
endPage: number,
pages: number[],
pageBrowser: boolean,
useful: boolean,
}

View file

@ -0,0 +1,7 @@
import { formatDate } from '@angular/common';
export function getCurrentDate(): string {
const now = new Date();
return formatDate(now, 'dd/MM/yyyy HH:mm:ss', 'fr-FR');
}

View file

@ -0,0 +1,8 @@
export function areObjectsEqual(obj1: any, obj2: any) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
return keys1.every(key => obj1[key] === obj2[key]);
}

View file

@ -0,0 +1,5 @@
export function addFilterParam(params: URLSearchParams, key: string, value: any): void {
if (value !== null && value !== undefined && value !== '') {
params.set(key, encodeURIComponent(value));
}
}

Some files were not shown because too many files have changed in this diff Show more