Sprint 6: JSON file persistence + real SSH terminal + V1 crypto/knownhosts integration

- go.mod replace directive for git.tukangketik.id/swanadiva/hostkeeper → ../../v1
- store/ package: JSON file persistence for hosts, snippets, keys, settings
  (~/Library/Application Support/hostkeeper/v2/)
- model SSH fields: Hostname, Port, Username, AuthType, Password, KeyID
- sshconn/ package: real SSH client via golang.org/x/crypto/ssh
- Terminal WS: real SSH connection with fallback to mock shell
- Dynamic host list in terminal (server-rendered JSON script tag)
- All mock data defaults removed from model packages
- Settings persist via store/settings.go
This commit is contained in:
swanadiva
2026-07-07 13:53:50 +07:00
parent 04d9cfb096
commit fd7361aa14
21 changed files with 746 additions and 306 deletions
+129
View File
@@ -0,0 +1,129 @@
package store
import "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
type HostStore struct {
hosts []model.Host
}
func NewHostStore() *HostStore {
s := &HostStore{}
mu.RLock()
readFile("hosts.json", &s.hosts)
mu.RUnlock()
if s.hosts == nil {
s.hosts = []model.Host{}
}
return s
}
func (s *HostStore) All() []model.Host {
mu.RLock()
defer mu.RUnlock()
return s.hosts
}
func (s *HostStore) Search(q, filter string) []model.Host {
mu.RLock()
defer mu.RUnlock()
var result []model.Host
for _, h := range s.hosts {
if filter != "" && filter != "all" && h.Status != filter {
continue
}
if q == "" || contains(h.Name, q) || contains(h.IP, q) || contains(h.OS, q) ||
contains(h.Hostname, q) || contains(h.Username, q) {
result = append(result, h)
}
}
if result == nil {
return []model.Host{}
}
return result
}
func (s *HostStore) Get(id string) (model.Host, bool) {
mu.RLock()
defer mu.RUnlock()
for _, h := range s.hosts {
if h.ID == id {
return h, true
}
}
return model.Host{}, false
}
func (s *HostStore) Add(name, ip, os, provider, hostType string) model.Host {
return s.AddFull(model.Host{
Name: name,
IP: ip,
OS: os,
Provider: provider,
Type: hostType,
})
}
func (s *HostStore) AddFull(h model.Host) model.Host {
mu.Lock()
defer mu.Unlock()
if h.ID == "" {
h.ID = randID()
}
if h.Status == "" {
h.Status = "active"
}
s.hosts = append(s.hosts, h)
writeFile("hosts.json", &s.hosts)
return h
}
func (s *HostStore) Update(h model.Host) bool {
mu.Lock()
defer mu.Unlock()
for i, existing := range s.hosts {
if existing.ID == h.ID {
s.hosts[i] = h
writeFile("hosts.json", &s.hosts)
return true
}
}
return false
}
func (s *HostStore) Delete(id string) {
mu.Lock()
defer mu.Unlock()
for i, h := range s.hosts {
if h.ID == id {
s.hosts = append(s.hosts[:i], s.hosts[i+1:]...)
writeFile("hosts.json", &s.hosts)
return
}
}
}
func contains(s, q string) bool {
if len(s) < len(q) {
return false
}
for i := 0; i <= len(s)-len(q); i++ {
match := true
for j := 0; j < len(q); j++ {
sc, qc := s[i+j], q[j]
if sc >= 'A' && sc <= 'Z' {
sc += 32
}
if qc >= 'A' && qc <= 'Z' {
qc += 32
}
if sc != qc {
match = false
break
}
}
if match {
return true
}
}
return false
}