Files
waste-go/internal/store/store.go
Fredrik Johansson 7fe02e9463 Fix web UI messaging, alias resolution, and onboarding
Messaging:
- Append \n to WS IPC sends so bufio.Scanner fires (messages from web
  were silently dropped — the line scanner never saw a newline)
- Handle session_ready in web store to update peer aliases after hello

Alias resolution:
- Seed AddPeer alias from SQLite cache so returning peers resolve
  immediately, before hello exchange completes
- Remove stale SavePeer call from AddPeer (was writing placeholder IDs)
- Add PeerAlias() point-lookup to store

Onboarding redesign:
- Three-state UI: disconnected (daemon instructions), connecting,
  connected-no-network (join form)
- Read ?n= / ?network= / ?anchor= / ?invite=waste:... URL params to
  pre-fill the join form for invite links
- Show daemon alias + peer ID before any network is joined
  (master_alias + master_id now included in state_snapshot)
- Identity backup: export (passphrase-protected yaw-key-backup-1 JSON,
  download button) and restore (paste + passphrase verify)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 23:22:32 +02:00

139 lines
3.7 KiB
Go

// Package store persists messages and peer info to a local SQLite database.
// Each daemon has its own database; nothing is shared across the network.
package store
import (
"database/sql"
"fmt"
"time"
_ "modernc.org/sqlite"
"github.com/waste-go/internal/proto"
)
const schema = `
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mid TEXT NOT NULL UNIQUE,
room TEXT NOT NULL,
from_peer TEXT NOT NULL,
body TEXT NOT NULL,
sent_at DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_room ON messages (room, sent_at);
CREATE TABLE IF NOT EXISTS peers (
peer_id TEXT PRIMARY KEY,
alias TEXT NOT NULL,
last_seen DATETIME NOT NULL
);
`
// Store is a local SQLite-backed message and peer store.
type Store struct {
db *sql.DB
}
// Open opens (or creates) the SQLite database at path and runs migrations.
func Open(path string) (*Store, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
db.SetMaxOpenConns(1) // SQLite is single-writer
if _, err := db.Exec(schema); err != nil {
db.Close()
return nil, fmt.Errorf("migrate db: %w", err)
}
return &Store{db: db}, nil
}
// Close releases the database connection.
func (s *Store) Close() error {
return s.db.Close()
}
// SaveMessage persists a chat message. Duplicate mids are silently ignored
// (INSERT OR IGNORE), so calling this more than once is safe.
func (s *Store) SaveMessage(msg *proto.ChatMessage) error {
sentAt := time.UnixMilli(msg.Ts).UTC()
_, err := s.db.Exec(
`INSERT OR IGNORE INTO messages (mid, room, from_peer, body, sent_at)
VALUES (?, ?, ?, ?, ?)`,
msg.Mid, msg.Room, string(msg.From), msg.Text, sentAt,
)
return err
}
// SavePeer upserts a peer's alias and last-seen timestamp.
func (s *Store) SavePeer(peerID proto.PeerID, alias string) error {
_, err := s.db.Exec(
`INSERT INTO peers (peer_id, alias, last_seen)
VALUES (?, ?, ?)
ON CONFLICT(peer_id) DO UPDATE SET alias=excluded.alias, last_seen=excluded.last_seen`,
string(peerID), alias, time.Now().UTC(),
)
return err
}
// PeerAlias returns the cached alias for a peer, or "" if unknown.
func (s *Store) PeerAlias(peerID proto.PeerID) string {
var alias string
s.db.QueryRow(`SELECT alias FROM peers WHERE peer_id = ?`, string(peerID)).Scan(&alias) //nolint:errcheck
return alias
}
// RecentMessages returns up to limit messages for a room, oldest first.
func (s *Store) RecentMessages(room string, limit int) ([]proto.ChatMessage, error) {
rows, err := s.db.Query(
`SELECT mid, from_peer, body, sent_at
FROM messages
WHERE room = ?
ORDER BY sent_at DESC
LIMIT ?`,
room, limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var msgs []proto.ChatMessage
for rows.Next() {
var m proto.ChatMessage
var from string
var sentAt time.Time
if err := rows.Scan(&m.Mid, &from, &m.Text, &sentAt); err != nil {
return nil, err
}
m.From = proto.PeerID(from)
m.Room = room
m.Ts = sentAt.UnixMilli()
msgs = append(msgs, m)
}
// Reverse so oldest-first.
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
msgs[i], msgs[j] = msgs[j], msgs[i]
}
return msgs, rows.Err()
}
// KnownPeers returns all peers seen since this daemon started storing data.
func (s *Store) KnownPeers() (map[proto.PeerID]string, error) {
rows, err := s.db.Query(`SELECT peer_id, alias FROM peers`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[proto.PeerID]string)
for rows.Next() {
var id, alias string
if err := rows.Scan(&id, &alias); err != nil {
return nil, err
}
out[proto.PeerID(id)] = alias
}
return out, rows.Err()
}