feat(v2): T-309 SQLite fall-event persistence (mattn/go-sqlite3)
Persist confirmed fall events to a local SQLite DB under artifacts/: camera host/port/channel + event id/track/source/config version/state/ latency/screenshot path/confirmed-at. Schema stores no password (guarded by test). Wire openRecorder/saveRecord into the alerts loop; failures are non-fatal. Re-applied the SILVER-POSE vendored os_windows.go patch that go mod vendor reverted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,9 @@ type SourceConfig struct {
|
||||
ID string
|
||||
URL string
|
||||
RTSPURLEnv string
|
||||
Host string
|
||||
Port int
|
||||
Channel string
|
||||
Transport string
|
||||
Timeout time.Duration
|
||||
LowLatency bool
|
||||
@@ -202,11 +205,31 @@ func resolveSource(raw rawSource) (SourceConfig, error) {
|
||||
}
|
||||
|
||||
if raw.Host != nil {
|
||||
streamURL, err := structuredSourceURL(raw)
|
||||
if err != nil {
|
||||
return SourceConfig{}, err
|
||||
host := strings.TrimSpace(*raw.Host)
|
||||
if host == "" {
|
||||
return SourceConfig{}, fmt.Errorf("source.host must be non-empty")
|
||||
}
|
||||
resolved.URL = streamURL
|
||||
if raw.Username == nil || strings.TrimSpace(*raw.Username) == "" {
|
||||
return SourceConfig{}, fmt.Errorf("source.username must be non-empty")
|
||||
}
|
||||
if raw.Password == nil || *raw.Password == "" {
|
||||
return SourceConfig{}, fmt.Errorf("source.password must be non-empty")
|
||||
}
|
||||
port := 554
|
||||
if raw.Port != nil {
|
||||
port = *raw.Port
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return SourceConfig{}, fmt.Errorf("source.port must be between 1 and 65535")
|
||||
}
|
||||
channel := "101"
|
||||
if raw.Channel != nil && strings.TrimSpace(*raw.Channel) != "" {
|
||||
channel = strings.TrimSpace(*raw.Channel)
|
||||
}
|
||||
resolved.Host = host
|
||||
resolved.Port = port
|
||||
resolved.Channel = channel
|
||||
resolved.URL = BuildRTSPURL(host, port, strings.TrimSpace(*raw.Username), *raw.Password, channel)
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
@@ -223,34 +246,6 @@ func resolveSource(raw rawSource) (SourceConfig, error) {
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// structuredSourceURL builds an RTSP URL from local host/credential fields.
|
||||
// net/url percent-encodes the userinfo, so a password with @/:/ does not break
|
||||
// parsing. These fields are only permitted in the untracked local config.
|
||||
func structuredSourceURL(raw rawSource) (string, error) {
|
||||
host := strings.TrimSpace(*raw.Host)
|
||||
if host == "" {
|
||||
return "", fmt.Errorf("source.host must be non-empty")
|
||||
}
|
||||
if raw.Username == nil || strings.TrimSpace(*raw.Username) == "" {
|
||||
return "", fmt.Errorf("source.username must be non-empty")
|
||||
}
|
||||
if raw.Password == nil || *raw.Password == "" {
|
||||
return "", fmt.Errorf("source.password must be non-empty")
|
||||
}
|
||||
port := 554
|
||||
if raw.Port != nil {
|
||||
port = *raw.Port
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return "", fmt.Errorf("source.port must be between 1 and 65535")
|
||||
}
|
||||
channel := "101"
|
||||
if raw.Channel != nil && strings.TrimSpace(*raw.Channel) != "" {
|
||||
channel = strings.TrimSpace(*raw.Channel)
|
||||
}
|
||||
return BuildRTSPURL(host, port, strings.TrimSpace(*raw.Username), *raw.Password, channel), nil
|
||||
}
|
||||
|
||||
// BuildRTSPURL assembles a Hikvision RTSP URL with percent-encoded credentials.
|
||||
func BuildRTSPURL(host string, port int, username, password, channel string) string {
|
||||
address := url.URL{
|
||||
|
||||
Reference in New Issue
Block a user