mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
* fix(history): stop reporting the device IP on every change (BEA-43) The history API embedded the whole DeviceInfo, so every project member read a teammate's server-observed IP (plus user/last_seen) next to every change on a page whose job is "who changed this file". Project a three-field historyDevice instead — id/name/os — mirroring heatByDevice. The device registry is unchanged: Observe/requestIP and both MetaStore backends keep recording the IP. This is a response projection, not a change in what gets collected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: history shows device name and OS, not the IP (BEA-43) README, SKILL.md and the docs site all promised the History view would show the connecting IP. It no longer does — the registry still records it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package webapp
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// DeviceInfo is what the server knows about one syncing device: self-reported
|
|
// name/OS (headers sent by the client), plus what the server itself observed
|
|
// (public IP of the last push, last activity, the signed-in account). History
|
|
// joins ops against this registry so ops stay small — but it reports only
|
|
// id/name/os (historyDevice, history.go): the IP is recorded here, not
|
|
// repeated to every project member on every change.
|
|
type DeviceInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name,omitempty"`
|
|
OS string `json:"os,omitempty"`
|
|
User string `json:"user,omitempty"` // account email last seen using this device
|
|
IP string `json:"ip,omitempty"` // as observed by the server
|
|
LastSeen time.Time `json:"last_seen"`
|
|
}
|
|
|
|
// DeviceRegistry is the in-memory device table over a MetaStore DeviceRepo.
|
|
type DeviceRegistry struct {
|
|
repo DeviceRepo
|
|
|
|
mu sync.Mutex
|
|
byID map[string]DeviceInfo
|
|
lastSav map[string]time.Time // throttle writes per device
|
|
}
|
|
|
|
// NewDeviceRegistry builds the registry over a repo, loading its contents.
|
|
func NewDeviceRegistry(repo DeviceRepo) (*DeviceRegistry, error) {
|
|
r := &DeviceRegistry{repo: repo, byID: make(map[string]DeviceInfo), lastSav: make(map[string]time.Time)}
|
|
list, err := repo.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, d := range list {
|
|
r.byID[d.ID] = d
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// OpenDeviceRegistry loads the file-backed registry at path.
|
|
func OpenDeviceRegistry(path string) (*DeviceRegistry, error) {
|
|
return NewDeviceRegistry(newFileDeviceRepo(path))
|
|
}
|
|
|
|
// Observe merges what a request revealed about a device. Disk writes are
|
|
// throttled: identity changes persist immediately, bare last-seen bumps at
|
|
// most once a minute.
|
|
func (r *DeviceRegistry) Observe(d DeviceInfo) {
|
|
if r == nil || d.ID == "" {
|
|
return
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cur := r.byID[d.ID]
|
|
changed := cur.ID == "" || cur.Name != d.Name || cur.OS != d.OS || cur.User != d.User || cur.IP != d.IP
|
|
cur.ID = d.ID
|
|
if d.Name != "" {
|
|
cur.Name = d.Name
|
|
}
|
|
if d.OS != "" {
|
|
cur.OS = d.OS
|
|
}
|
|
if d.User != "" {
|
|
cur.User = d.User
|
|
}
|
|
if d.IP != "" {
|
|
cur.IP = d.IP
|
|
}
|
|
cur.LastSeen = time.Now().UTC()
|
|
r.byID[d.ID] = cur
|
|
if changed || time.Since(r.lastSav[d.ID]) > time.Minute {
|
|
if r.repo.Put(cur) == nil {
|
|
r.lastSav[d.ID] = time.Now()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *DeviceRegistry) Get(id string) (DeviceInfo, bool) {
|
|
if r == nil {
|
|
return DeviceInfo{}, false
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
d, ok := r.byID[id]
|
|
return d, ok
|
|
}
|
|
|
|
// requestIP extracts the client address the server actually saw.
|
|
func requestIP(r *http.Request) string {
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
return strings.TrimSpace(strings.Split(xff, ",")[0])
|
|
}
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
// observeDevice records the device behind a store-API request.
|
|
func (s *Server) observeDevice(r *http.Request) {
|
|
if s.Devices == nil {
|
|
return
|
|
}
|
|
id := r.Header.Get("X-Bdrive-Device")
|
|
if id == "" {
|
|
return
|
|
}
|
|
s.Devices.Observe(DeviceInfo{
|
|
ID: id,
|
|
Name: r.Header.Get("X-Bdrive-Device-Name"),
|
|
OS: r.Header.Get("X-Bdrive-Os"),
|
|
User: s.requestUser(r).Email,
|
|
IP: requestIP(r),
|
|
})
|
|
}
|