Analytics: time-series message graph with 5 time spans (1h/4h/24h/7d/30d), top-5 agents and channels leaderboards, summary cards. 4 new REST endpoints. PWA: web app manifest, service worker with cache-first static/network-only API strategy, push notifications via Web Push API with VAPID keys, push subscription management endpoints, SQLite migration for subscriptions. UX fixes: auto-resize compose textarea (3-12 lines), inline editable agent display name, editable human display name in settings, smart mention/channel highlighting (existing→link, deleted→inactive badge, unknown→plain text), font size -/+ preference (12-24px persisted in localStorage), version footer with GitHub link. MCP: 4 prompts — daily-digest, agent-health-check, channel-overview, debug-agent. Registered with prompt capabilities enabled. Code review fixes: scoped push unsubscribe to user, capped analytics limit at 100, hardened HTML strip regex, bounded SW cache, backend push unsub on disable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
105 lines
3.3 KiB
Go
105 lines
3.3 KiB
Go
// Package push provides Web Push notification support for SynapBus.
|
|
package push
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
)
|
|
|
|
// Store defines the persistence interface for push subscriptions.
|
|
type Store interface {
|
|
// Subscribe registers a push subscription for a user.
|
|
// If the endpoint already exists, it updates the keys and user agent.
|
|
Subscribe(ctx context.Context, userID int64, endpoint, keyP256dh, keyAuth, userAgent string) error
|
|
|
|
// Unsubscribe removes a push subscription by endpoint, scoped to user.
|
|
Unsubscribe(ctx context.Context, userID int64, endpoint string) error
|
|
|
|
// GetSubscriptions returns all push subscriptions for a user.
|
|
GetSubscriptions(ctx context.Context, userID int64) ([]Subscription, error)
|
|
|
|
// DeleteSubscription removes a push subscription by endpoint.
|
|
// This is used to clean up stale subscriptions (e.g., 410 Gone responses).
|
|
DeleteSubscription(ctx context.Context, endpoint string) error
|
|
}
|
|
|
|
// Subscription represents a Web Push subscription stored in the database.
|
|
type Subscription struct {
|
|
ID int64
|
|
UserID int64
|
|
Endpoint string
|
|
KeyP256dh string
|
|
KeyAuth string
|
|
UserAgent string
|
|
CreatedAt string
|
|
}
|
|
|
|
// SQLiteStore implements Store using SQLite.
|
|
type SQLiteStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewSQLiteStore creates a new SQLite-backed push subscription store.
|
|
func NewSQLiteStore(db *sql.DB) *SQLiteStore {
|
|
return &SQLiteStore{db: db}
|
|
}
|
|
|
|
// Subscribe registers or updates a push subscription for a user.
|
|
func (s *SQLiteStore) Subscribe(ctx context.Context, userID int64, endpoint, keyP256dh, keyAuth, userAgent string) error {
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO push_subscriptions (user_id, endpoint, key_p256dh, key_auth, user_agent)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(endpoint) DO UPDATE SET
|
|
key_p256dh = excluded.key_p256dh,
|
|
key_auth = excluded.key_auth,
|
|
user_agent = excluded.user_agent,
|
|
user_id = excluded.user_id`,
|
|
userID, endpoint, keyP256dh, keyAuth, userAgent,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// Unsubscribe removes a push subscription by endpoint, scoped to the owning user.
|
|
func (s *SQLiteStore) Unsubscribe(ctx context.Context, userID int64, endpoint string) error {
|
|
_, err := s.db.ExecContext(ctx,
|
|
`DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?`,
|
|
userID, endpoint,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// GetSubscriptions returns all push subscriptions for a user.
|
|
func (s *SQLiteStore) GetSubscriptions(ctx context.Context, userID int64) ([]Subscription, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, user_id, endpoint, key_p256dh, key_auth, user_agent, created_at
|
|
FROM push_subscriptions
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC`,
|
|
userID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var subs []Subscription
|
|
for rows.Next() {
|
|
var sub Subscription
|
|
if err := rows.Scan(&sub.ID, &sub.UserID, &sub.Endpoint, &sub.KeyP256dh, &sub.KeyAuth, &sub.UserAgent, &sub.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
subs = append(subs, sub)
|
|
}
|
|
return subs, rows.Err()
|
|
}
|
|
|
|
// DeleteSubscription removes a push subscription by endpoint (any user).
|
|
// Used for stale subscription cleanup (e.g., 410 Gone responses).
|
|
func (s *SQLiteStore) DeleteSubscription(ctx context.Context, endpoint string) error {
|
|
_, err := s.db.ExecContext(ctx,
|
|
`DELETE FROM push_subscriptions WHERE endpoint = ?`,
|
|
endpoint,
|
|
)
|
|
return err
|
|
}
|