From 5ac0af92f19c2dde4047ebaeb9156a9c062d97c1 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 22 Jan 2025 22:46:53 -0500 Subject: [PATCH 01/58] Leftover rule builder changes --- .../src/app/incidents/incident/incident.component.ts | 2 +- client/stillbox/src/app/talkgroup.ts | 4 ---- client/stillbox/src/app/talkgroups/talkgroups.service.ts | 5 +---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 944fe04..ad5cdcc 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, Sanitizer } from '@angular/core'; import { tap } from 'rxjs/operators'; import { CommonModule, Location } from '@angular/common'; import { BehaviorSubject, merge, Subscription } from 'rxjs'; diff --git a/client/stillbox/src/app/talkgroup.ts b/client/stillbox/src/app/talkgroup.ts index 7c8b2f7..02ba326 100644 --- a/client/stillbox/src/app/talkgroup.ts +++ b/client/stillbox/src/app/talkgroup.ts @@ -91,10 +91,6 @@ export class Talkgroup { icon?: string, ) { this.iconSvg = this.iconMap(this.metadata?.icon!); - this.alert_rules = this.alert_rules.map((x) => - Object.assign(new AlertRule(), x), - ); - console.log(this.alert_rules); } iconMap(icon: string): string { diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 0896b64..7a41161 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -33,10 +33,7 @@ export class TalkgroupService { private subscriptions = new Subscription(); constructor(private http: HttpClient) { this.tgs$ = this.fetchAll.pipe(switchMap(() => this.getTalkgroups())); - this.tags$ = this.fetchAll.pipe( - switchMap(() => this.getAllTags()), - shareReplay(), - ); + this.tags$ = this.fetchAll.pipe(switchMap(() => this.getAllTags()), shareReplay()); this.fillTgMap(); } -- 2.48.1 From 4e37e14a9603510651be633ecc0261b9255742a1 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 22 Jan 2025 22:58:24 -0500 Subject: [PATCH 02/58] Initial share route --- client/stillbox/src/app/app.routes.ts | 5 ++++ .../src/app/share/share.component.html | 1 + .../src/app/share/share.component.scss | 0 .../src/app/share/share.component.spec.ts | 23 +++++++++++++++++++ .../stillbox/src/app/share/share.component.ts | 11 +++++++++ 5 files changed, 40 insertions(+) create mode 100644 client/stillbox/src/app/share/share.component.html create mode 100644 client/stillbox/src/app/share/share.component.scss create mode 100644 client/stillbox/src/app/share/share.component.spec.ts create mode 100644 client/stillbox/src/app/share/share.component.ts diff --git a/client/stillbox/src/app/app.routes.ts b/client/stillbox/src/app/app.routes.ts index 9898a2e..a904bb2 100644 --- a/client/stillbox/src/app/app.routes.ts +++ b/client/stillbox/src/app/app.routes.ts @@ -8,6 +8,11 @@ export const routes: Routes = [ loadComponent: () => import('./login/login.component').then((m) => m.LoginComponent), }, + { + path: 's/:id', + loadComponent: () => + import('./share/share.component').then((m) => m.ShareComponent), + }, { path: '', canActivateChild: [AuthGuard], diff --git a/client/stillbox/src/app/share/share.component.html b/client/stillbox/src/app/share/share.component.html new file mode 100644 index 0000000..5ca71b6 --- /dev/null +++ b/client/stillbox/src/app/share/share.component.html @@ -0,0 +1 @@ +

share works!

diff --git a/client/stillbox/src/app/share/share.component.scss b/client/stillbox/src/app/share/share.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/stillbox/src/app/share/share.component.spec.ts b/client/stillbox/src/app/share/share.component.spec.ts new file mode 100644 index 0000000..6115935 --- /dev/null +++ b/client/stillbox/src/app/share/share.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShareComponent } from './share.component'; + +describe('ShareComponent', () => { + let component: ShareComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ShareComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ShareComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/stillbox/src/app/share/share.component.ts b/client/stillbox/src/app/share/share.component.ts new file mode 100644 index 0000000..bd63b0d --- /dev/null +++ b/client/stillbox/src/app/share/share.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-share', + imports: [], + templateUrl: './share.component.html', + styleUrl: './share.component.scss' +}) +export class ShareComponent { + +} -- 2.48.1 From 6bd30a9dd6bc22569a7e2c1f610da0292dd92ad5 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 26 Jan 2025 12:20:30 -0500 Subject: [PATCH 03/58] wip --- .../stillbox/src/app/share/share.component.ts | 9 ++++++ .../src/app/share/share.service.spec.ts | 16 ++++++++++ .../stillbox/src/app/share/share.service.ts | 30 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 client/stillbox/src/app/share/share.service.spec.ts create mode 100644 client/stillbox/src/app/share/share.service.ts diff --git a/client/stillbox/src/app/share/share.component.ts b/client/stillbox/src/app/share/share.component.ts index bd63b0d..2f28cfc 100644 --- a/client/stillbox/src/app/share/share.component.ts +++ b/client/stillbox/src/app/share/share.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core'; +import { ShareService } from './share.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-share', @@ -7,5 +9,12 @@ import { Component } from '@angular/core'; styleUrl: './share.component.scss' }) export class ShareComponent { + constructor( + private route: ActivatedRoute, + private shareSvc: ShareService, + ) {} + ngOnInit() { + + } } diff --git a/client/stillbox/src/app/share/share.service.spec.ts b/client/stillbox/src/app/share/share.service.spec.ts new file mode 100644 index 0000000..37145a5 --- /dev/null +++ b/client/stillbox/src/app/share/share.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ShareService } from './share.service'; + +describe('ShareService', () => { + let service: ShareService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ShareService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts new file mode 100644 index 0000000..4ea55f0 --- /dev/null +++ b/client/stillbox/src/app/share/share.service.ts @@ -0,0 +1,30 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map, Observable, switchMap } from 'rxjs'; +import { IncidentRecord } from '../incidents'; + +type Share = IncidentRecord | ArrayBuffer; +@Injectable({ + providedIn: 'root' +}) +export class ShareService { + + constructor( + private http: HttpClient, + ) { } + + getShare(id: string): Observable { + return this.http.get(`/share/${id}`, {observe: 'response'}).pipe( + map((res) => { + let typ = res.headers.get('X-Share-Type'); + switch(typ) { + case 'call': + return (res.body as ArrayBuffer); + case 'incident': + return (res.body as IncidentRecord); + } + return null; + }) + ); + } +} -- 2.48.1 From 6fc2019c9f093c118b428f3b297b6a15b6c5561c Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 20:16:01 -0500 Subject: [PATCH 04/58] exclude tgstats --- client/stillbox/ngsw-config.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/stillbox/ngsw-config.json b/client/stillbox/ngsw-config.json index 69edd28..490840e 100644 --- a/client/stillbox/ngsw-config.json +++ b/client/stillbox/ngsw-config.json @@ -26,5 +26,14 @@ ] } } + ], + "navigationUrls": + [ + "/**", + "!/**/*.*", + "!/**/****", + "!/**/****/**", + "!/tgstats", + "!/tgstats/**" ] } -- 2.48.1 From f54df4a1156c790ee9a78cbf1d620812e0707eaf Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 20:37:17 -0500 Subject: [PATCH 05/58] Catch TG errors, fix policy cycle --- client/stillbox/ngsw-config.json | 5 ++++- pkg/alerting/alerting.go | 7 ++++++- pkg/rbac/policy/policy.go | 11 ++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/client/stillbox/ngsw-config.json b/client/stillbox/ngsw-config.json index 490840e..fa63476 100644 --- a/client/stillbox/ngsw-config.json +++ b/client/stillbox/ngsw-config.json @@ -34,6 +34,9 @@ "!/**/****", "!/**/****/**", "!/tgstats", - "!/tgstats/**" + "!/tgstats/**", + "!/api/**", + "!/share/**", + "!/testnotify" ] } diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index e580aed..c67c572 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -166,7 +166,12 @@ func (as *alerter) eval(ctx context.Context, now time.Time, testMode bool) ([]al for _, s := range as.scores { origScore := s.Score tgr, err := as.tgCache.TG(ctx, s.ID) - if err != nil || !tgr.Talkgroup.Alert { + if err != nil { + log.Error().Err(err).Msg("alerting eval tg get") + continue + } + + if!tgr.Talkgroup.Alert { continue } diff --git a/pkg/rbac/policy/policy.go b/pkg/rbac/policy/policy.go index 16798a6..fab644c 100644 --- a/pkg/rbac/policy/policy.go +++ b/pkg/rbac/policy/policy.go @@ -81,16 +81,25 @@ var Policy = &restrict.PolicyDefinition{ Parents: []string{entities.RoleUser}, Grants: restrict.GrantsMap{ entities.ResourceIncident: { + &restrict.Permission{Action: entities.ActionRead}, &restrict.Permission{Action: entities.ActionUpdate}, &restrict.Permission{Action: entities.ActionDelete}, &restrict.Permission{Action: entities.ActionShare}, }, entities.ResourceCall: { + &restrict.Permission{Action: entities.ActionRead}, &restrict.Permission{Action: entities.ActionUpdate}, &restrict.Permission{Action: entities.ActionDelete}, &restrict.Permission{Action: entities.ActionShare}, }, entities.ResourceTalkgroup: { + &restrict.Permission{Action: entities.ActionRead}, + &restrict.Permission{Action: entities.ActionUpdate}, + &restrict.Permission{Action: entities.ActionCreate}, + &restrict.Permission{Action: entities.ActionDelete}, + }, + entities.ResourceShare: { + &restrict.Permission{Action: entities.ActionRead}, &restrict.Permission{Action: entities.ActionUpdate}, &restrict.Permission{Action: entities.ActionCreate}, &restrict.Permission{Action: entities.ActionDelete}, @@ -98,7 +107,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, entities.RoleSystem: { - Parents: []string{entities.RoleSystem}, + Parents: []string{entities.RoleAdmin}, }, entities.RolePublic: { /* -- 2.48.1 From cde862a2e72375942701aa75da24bc016380980d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 20:51:58 -0500 Subject: [PATCH 06/58] Hupper --- pkg/alerting/alerting.go | 21 ++++++++++++++++++--- pkg/server/signals.go | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index c67c572..e4bf912 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -38,6 +38,7 @@ type Alerter interface { Enabled() bool Go(context.Context) + HUP(*config.Config) stats } @@ -101,9 +102,7 @@ func New(cfg config.Alerting, tgCache tgstore.Store, opts ...AlertOption) Alerte tgCache: tgCache, } - if cfg.Renotify != nil { - as.renotify = cfg.Renotify.Duration() - } + as.reload() for _, opt := range opts { opt(as) @@ -122,6 +121,21 @@ func New(cfg config.Alerting, tgCache tgstore.Store, opts ...AlertOption) Alerte return as } +func (as *alerter) reload() { + if as.cfg.Renotify != nil { + as.renotify = as.cfg.Renotify.Duration() + } +} + +func (as *alerter) HUP(cfg *config.Config) { + as.Lock() + defer as.Unlock() + + log.Debug().Msg("reloading alert config") + as.cfg = cfg.Alerting + as.reload() +} + // Go is the alerting loop. It does not start a goroutine. func (as *alerter) Go(ctx context.Context) { ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "alerter"}) @@ -381,3 +395,4 @@ func (*noopAlerter) SinkType() string { return "noopA func (*noopAlerter) Call(_ context.Context, _ *calls.Call) error { return nil } func (*noopAlerter) Go(_ context.Context) {} func (*noopAlerter) Enabled() bool { return false } +func (*noopAlerter) HUP(_ *config.Config) { } diff --git a/pkg/server/signals.go b/pkg/server/signals.go index b3a60a8..8cf9c64 100644 --- a/pkg/server/signals.go +++ b/pkg/server/signals.go @@ -18,6 +18,7 @@ func (s *Server) huppers() []hupper { s.logger, s.auth, s.tgs, + s.alerter, } } -- 2.48.1 From a2e080acc8feaa0d7984c3a23accf40170a882f4 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 20:56:03 -0500 Subject: [PATCH 07/58] Description --- pkg/rbac/policy/policy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/rbac/policy/policy.go b/pkg/rbac/policy/policy.go index fab644c..d2721e4 100644 --- a/pkg/rbac/policy/policy.go +++ b/pkg/rbac/policy/policy.go @@ -78,6 +78,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, entities.RoleAdmin: { + Description: "A superuser", Parents: []string{entities.RoleUser}, Grants: restrict.GrantsMap{ entities.ResourceIncident: { @@ -107,6 +108,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, entities.RoleSystem: { + Description: "A system service", Parents: []string{entities.RoleAdmin}, }, entities.RolePublic: { -- 2.48.1 From 8f24b41b3e464f522ae895ecb6887bb23e602b1b Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 21:03:58 -0500 Subject: [PATCH 08/58] support m3u --- pkg/rest/share.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 1ffba53..ebbd63f 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -79,6 +79,7 @@ func (sa *shareAPI) RootRouter() http.Handler { r.Get("/{shareId:[A-Za-z0-9_-]{20,}}", sa.routeShare) r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}", sa.routeShare) + r.Get("/{shareId:[A-Za-z0-9_-]{20,}}.{type}", sa.routeShare) r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}/{subID}", sa.routeShare) return r } -- 2.48.1 From 98ff7b031fb9cc44139c639e53f9c42550021469 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 21:14:13 -0500 Subject: [PATCH 09/58] improve m3u --- pkg/incidents/incident.go | 6 ++++++ pkg/rest/incidents.go | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index 896c083..85eaeaf 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -2,6 +2,7 @@ package incidents import ( "encoding/json" + "strings" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/calls" @@ -26,6 +27,11 @@ func (inc *Incident) GetResourceName() string { return entities.ResourceIncident } +func (inc *Incident) PlaylistFilename() string { + rep := strings.NewReplacer(" ", "_", "/", "_", ":", "_") + return rep.Replace(strings.ToLower(inc.Name)) +} + type IncidentCall struct { calls.Call Notes json.RawMessage `json:"notes"` diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index 6baf4a2..ff850b0 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -213,9 +213,12 @@ func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseW callUrl := common.PtrTo(*ia.baseURL) urlRoot := "/api/call" + filename := inc.PlaylistFilename() if share != nil { urlRoot = fmt.Sprintf("/share/%s/call/", share.ID) + filename += "_" + share.ID } + filename += ".m3u" b.WriteString("#EXTM3U\n\n") for _, c := range inc.Calls { @@ -243,6 +246,7 @@ func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseW // Not a lot of agreement on which MIME type to use for non-HLS m3u, // let's hope this is good enough w.Header().Set("Content-Type", "audio/x-mpegurl") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) w.WriteHeader(http.StatusOK) _, _ = b.WriteTo(w) } -- 2.48.1 From 493913112dfb4458081d387453ca3d585a3388e7 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 29 Jan 2025 22:29:21 -0500 Subject: [PATCH 10/58] omitempty --- pkg/incidents/incident.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index 85eaeaf..0cc378b 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -15,11 +15,11 @@ type Incident struct { ID uuid.UUID `json:"id"` Owner users.UserID `json:"owner"` Name string `json:"name"` - Description *string `json:"description"` - StartTime *jsontypes.Time `json:"startTime"` - EndTime *jsontypes.Time `json:"endTime"` - Location jsontypes.Location `json:"location"` - Metadata jsontypes.Metadata `json:"metadata"` + Description *string `json:"description,omitempty"` + StartTime *jsontypes.Time `json:"startTime,omitempty"` + EndTime *jsontypes.Time `json:"endTime,omitempty"` + Location jsontypes.Location `json:"location,omitempty"` + Metadata jsontypes.Metadata `json:"metadata,omitempty"` Calls []IncidentCall `json:"calls"` } @@ -34,5 +34,5 @@ func (inc *Incident) PlaylistFilename() string { type IncidentCall struct { calls.Call - Notes json.RawMessage `json:"notes"` + Notes json.RawMessage `json:"notes,omitempty"` } -- 2.48.1 From 1232b6887abe9e89dde318a9ff357dc311aff700 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 1 Feb 2025 14:28:01 -0500 Subject: [PATCH 11/58] Get rid of using headers to identify share type --- pkg/alerting/alerting.go | 4 +- pkg/calls/callstore/store.go | 2 +- pkg/rbac/policy/policy.go | 4 +- pkg/rest/api.go | 3 +- pkg/rest/calls.go | 11 +++-- pkg/rest/incidents.go | 23 +++++----- pkg/rest/share.go | 88 +++++++++++++++++++++++++----------- pkg/rest/talkgroups.go | 9 +++- 8 files changed, 96 insertions(+), 48 deletions(-) diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index e4bf912..769781b 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -185,7 +185,7 @@ func (as *alerter) eval(ctx context.Context, now time.Time, testMode bool) ([]al continue } - if!tgr.Talkgroup.Alert { + if !tgr.Talkgroup.Alert { continue } @@ -395,4 +395,4 @@ func (*noopAlerter) SinkType() string { return "noopA func (*noopAlerter) Call(_ context.Context, _ *calls.Call) error { return nil } func (*noopAlerter) Go(_ context.Context) {} func (*noopAlerter) Enabled() bool { return false } -func (*noopAlerter) HUP(_ *config.Config) { } +func (*noopAlerter) HUP(_ *config.Config) {} diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 85aed7a..633b166 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -145,7 +145,7 @@ func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, } func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) { - _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead)) + _, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } diff --git a/pkg/rbac/policy/policy.go b/pkg/rbac/policy/policy.go index d2721e4..16addcc 100644 --- a/pkg/rbac/policy/policy.go +++ b/pkg/rbac/policy/policy.go @@ -79,7 +79,7 @@ var Policy = &restrict.PolicyDefinition{ }, entities.RoleAdmin: { Description: "A superuser", - Parents: []string{entities.RoleUser}, + Parents: []string{entities.RoleUser}, Grants: restrict.GrantsMap{ entities.ResourceIncident: { &restrict.Permission{Action: entities.ActionRead}, @@ -109,7 +109,7 @@ var Policy = &restrict.PolicyDefinition{ }, entities.RoleSystem: { Description: "A system service", - Parents: []string{entities.RoleAdmin}, + Parents: []string{entities.RoleAdmin}, }, entities.RolePublic: { /* diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 8f0928c..0f249b6 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -51,8 +51,9 @@ func New(baseURL url.URL) *api { s.shares = newShareAPI(&baseURL, ShareHandlers{ ShareRequestCall: s.calls.shareCallRoute, + ShareRequestCallInfo: respondShareHandler(s.calls.getCallInfo), ShareRequestCallDL: s.calls.shareCallDLRoute, - ShareRequestIncident: s.incidents.getIncident, + ShareRequestIncident: respondShareHandler(s.incidents.getIncident), ShareRequestIncidentM3U: s.incidents.getCallsM3U, ShareRequestTalkgroups: s.tgs.getTGsShareRoute, }, diff --git a/pkg/rest/calls.go b/pkg/rest/calls.go index 1492f6c..cdbc5bf 100644 --- a/pkg/rest/calls.go +++ b/pkg/rest/calls.go @@ -1,6 +1,7 @@ package rest import ( + "context" "errors" "fmt" "mime" @@ -11,7 +12,6 @@ import ( "dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/pkg/calls/callstore" "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/shares" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -102,7 +102,12 @@ func (ca *callsAPI) getAudio(p getAudioParams, w http.ResponseWriter, r *http.Re _, _ = w.Write(call.AudioBlob) } -func (ca *callsAPI) shareCallRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) { +func (ca *callsAPI) getCallInfo(ctx context.Context, id ID) (SharedItem, error) { + cs := callstore.FromCtx(ctx) + return cs.Call(ctx, id.(uuid.UUID)) +} + +func (ca *callsAPI) shareCallRoute(id ID, w http.ResponseWriter, r *http.Request) { p := getAudioParams{ CallID: common.PtrTo(id.(uuid.UUID)), } @@ -110,7 +115,7 @@ func (ca *callsAPI) shareCallRoute(id ID, _ *shares.Share, w http.ResponseWriter ca.getAudio(p, w, r) } -func (ca *callsAPI) shareCallDLRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) { +func (ca *callsAPI) shareCallDLRoute(id ID, w http.ResponseWriter, r *http.Request) { p := getAudioParams{ CallID: common.PtrTo(id.(uuid.UUID)), Download: common.PtrTo("download"), diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index ff850b0..ef52255 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -2,6 +2,7 @@ package rest import ( "bytes" + "context" "encoding/json" "fmt" "net/http" @@ -12,7 +13,6 @@ import ( "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/incidents/incstore" - "dynatron.me/x/stillbox/pkg/shares" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "github.com/go-chi/chi/v5" @@ -91,22 +91,22 @@ func (ia *incidentsAPI) createIncident(w http.ResponseWriter, r *http.Request) { func (ia *incidentsAPI) getIncidentRoute(w http.ResponseWriter, r *http.Request) { id, err := idOnlyParam(w, r) if err != nil { + wErr(w, r, autoError(err)) return } - ia.getIncident(id, nil, w, r) -} - -func (ia *incidentsAPI) getIncident(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - incs := incstore.FromCtx(ctx) - inc, err := incs.Incident(ctx, id.(uuid.UUID)) + e, err := ia.getIncident(r.Context(), id) if err != nil { wErr(w, r, autoError(err)) return } - respond(w, r, inc) + respond(w, r, e) +} + +func (ia *incidentsAPI) getIncident(ctx context.Context, id ID) (SharedItem, error) { + incs := incstore.FromCtx(ctx) + return incs.Incident(ctx, id.(uuid.UUID)) } func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) { @@ -195,10 +195,10 @@ func (ia *incidentsAPI) getCallsM3URoute(w http.ResponseWriter, r *http.Request) return } - ia.getCallsM3U(id, nil, w, r) + ia.getCallsM3U(id, w, r) } -func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) { +func (ia *incidentsAPI) getCallsM3U(id ID, w http.ResponseWriter, r *http.Request) { ctx := r.Context() incs := incstore.FromCtx(ctx) tgst := tgstore.FromCtx(ctx) @@ -214,6 +214,7 @@ func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseW callUrl := common.PtrTo(*ia.baseURL) urlRoot := "/api/call" filename := inc.PlaylistFilename() + share := ShareFrom(ctx) if share != nil { urlRoot = fmt.Sprintf("/share/%s/call/", share.ID) filename += "_" + share.ID diff --git a/pkg/rest/share.go b/pkg/rest/share.go index ebbd63f..44af365 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -1,6 +1,7 @@ package rest import ( + "context" "errors" "net/http" "net/url" @@ -23,6 +24,7 @@ type ShareRequestType string const ( ShareRequestCall ShareRequestType = "call" + ShareRequestCallInfo ShareRequestType = "callinfo" ShareRequestCallDL ShareRequestType = "callDL" ShareRequestIncident ShareRequestType = "incident" ShareRequestIncidentM3U ShareRequestType = "m3u" @@ -31,7 +33,7 @@ const ( func (rt ShareRequestType) IsValid() bool { switch rt { - case ShareRequestCall, ShareRequestCallDL, ShareRequestIncident, + case ShareRequestCall, ShareRequestCallInfo, ShareRequestCallDL, ShareRequestIncident, ShareRequestIncidentM3U, ShareRequestTalkgroups: return true } @@ -39,25 +41,59 @@ func (rt ShareRequestType) IsValid() bool { return false } -func (rt ShareRequestType) IsValidSubtype() bool { - switch rt { - case ShareRequestCall, ShareRequestCallDL, ShareRequestTalkgroups: - return true - } - - return false -} - type ID interface { } -type HandlerFunc func(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) -type ShareHandlers map[ShareRequestType]HandlerFunc +type ShareHandlerFunc func(id ID, w http.ResponseWriter, r *http.Request) +type ShareHandlers map[ShareRequestType]ShareHandlerFunc type shareAPI struct { baseURL *url.URL shnd ShareHandlers } +type EntityFunc func(ctx context.Context, id ID) (SharedItem, error) +type SharedItem interface { +} + +type shareResponse struct { + ID ID `json:"id"` + Type shares.EntityType `json:"type"` + Item SharedItem `json:"item,omitempty"` +} + +func ShareFrom(ctx context.Context) *shares.Share { + if share, hasShare := entities.SubjectFrom(ctx).(*shares.Share); hasShare { + return share + } + + return nil +} + +func respondShareHandler(ie EntityFunc) ShareHandlerFunc { + return func(id ID, w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + share := ShareFrom(ctx) + if share == nil { + wErr(w, r, autoError(ErrBadShare)) + return + } + + res, err := ie(r.Context(), id) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + sRes := shareResponse{ + ID: id, + Type: share.Type, + Item: res, + } + + respond(w, r, sRes) + } +} + func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI { return &shareAPI{ baseURL: baseURL, @@ -135,12 +171,11 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) { } else { switch sh.Type { case shares.EntityCall: - rType = ShareRequestCall + rType = ShareRequestCallInfo params.SubID = common.PtrTo(sh.EntityID.String()) case shares.EntityIncident: rType = ShareRequestIncident } - w.Header().Set("X-Share-Type", string(rType)) } if !rType.IsValid() { @@ -158,21 +193,22 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) { switch rType { case ShareRequestTalkgroups: - sa.shnd[rType](nil, sh, w, r) - case ShareRequestCall, ShareRequestCallDL: - if params.SubID == nil { - wErr(w, r, autoError(ErrBadShare)) - return + sa.shnd[rType](nil, w, r) + case ShareRequestCall, ShareRequestCallInfo, ShareRequestCallDL: + var subIDU uuid.UUID + if params.SubID != nil { + subIDU, err = uuid.Parse(*params.SubID) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + } else { + subIDU = sh.EntityID } - subIDU, err := uuid.Parse(*params.SubID) - if err != nil { - wErr(w, r, badRequest(err)) - return - } - sa.shnd[rType](subIDU, sh, w, r) + sa.shnd[rType](subIDU, w, r) case ShareRequestIncident, ShareRequestIncidentM3U: - sa.shnd[rType](sh.EntityID, sh, w, r) + sa.shnd[rType](sh.EntityID, w, r) } } diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index d97e9ea..8e251d8 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -8,7 +8,6 @@ import ( "dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/incidents/incstore" - "dynatron.me/x/stillbox/pkg/shares" "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "dynatron.me/x/stillbox/pkg/talkgroups/xport" @@ -161,10 +160,16 @@ func (tga *talkgroupAPI) postPaginated(w http.ResponseWriter, r *http.Request) { respond(w, r, res) } -func (tga *talkgroupAPI) getTGsShareRoute(_ ID, share *shares.Share, w http.ResponseWriter, r *http.Request) { +func (tga *talkgroupAPI) getTGsShareRoute(_ ID, w http.ResponseWriter, r *http.Request) { ctx := r.Context() tgs := tgstore.FromCtx(ctx) + share := ShareFrom(ctx) + if share == nil { + wErr(w, r, autoError(ErrBadShare)) + return + } + tgIDs, err := incstore.FromCtx(ctx).TGsIn(ctx, share.EntityID) if err != nil { wErr(w, r, autoError(err)) -- 2.48.1 From cf498d241a9fb545b19a51c580f3da6f3a2aa4f8 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 1 Feb 2025 21:02:23 -0500 Subject: [PATCH 12/58] Implement share DELETE --- pkg/rest/share.go | 20 ++++++++++++++++++++ pkg/shares/store.go | 7 ++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 44af365..647e837 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -213,4 +213,24 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) { } func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + shs := shares.FromCtx(ctx) + + p := struct { + ID string `param:"id"` + }{} + + err := decodeParams(&p, r) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + err = shs.Delete(ctx, p.ID) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + w.WriteHeader(http.StatusNoContent) } diff --git a/pkg/shares/store.go b/pkg/shares/store.go index d0fbad0..cf4e0d6 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -80,7 +80,12 @@ func (s *postgresStore) Create(ctx context.Context, share *Share) error { } func (s *postgresStore) Delete(ctx context.Context, id string) error { - _, err := rbac.Check(ctx, new(Share), rbac.WithActions(entities.ActionDelete)) + sh, err := s.GetShare(ctx, id) + if err != nil { + return err + } + + _, err = rbac.Check(ctx, sh, rbac.WithActions(entities.ActionDelete)) if err != nil { return err } -- 2.48.1 From 40ca65089507c2d0042b10f7b0940666fc655ead Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 1 Feb 2025 21:23:22 -0500 Subject: [PATCH 13/58] share API stuff into share.go --- pkg/rest/api.go | 12 +----------- pkg/rest/share.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 0f249b6..2a02206 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -48,17 +48,7 @@ func New(baseURL url.URL) *api { incidents: newIncidentsAPI(&baseURL), users: new(usersAPI), } - s.shares = newShareAPI(&baseURL, - ShareHandlers{ - ShareRequestCall: s.calls.shareCallRoute, - ShareRequestCallInfo: respondShareHandler(s.calls.getCallInfo), - ShareRequestCallDL: s.calls.shareCallDLRoute, - ShareRequestIncident: respondShareHandler(s.incidents.getIncident), - ShareRequestIncidentM3U: s.incidents.getCallsM3U, - ShareRequestTalkgroups: s.tgs.getTGsShareRoute, - }, - ) - + s.shares = newShareAPI(&baseURL, s.shareHandlers()) return s } diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 647e837..ce1d704 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -31,6 +31,18 @@ const ( ShareRequestTalkgroups ShareRequestType = "talkgroups" ) +// shareHandlers returns a ShareHandlers map from the api. +func (s *api) shareHandlers() ShareHandlers { + return ShareHandlers{ + ShareRequestCall: s.calls.shareCallRoute, + ShareRequestCallInfo: respondShareHandler(s.calls.getCallInfo), + ShareRequestCallDL: s.calls.shareCallDLRoute, + ShareRequestIncident: respondShareHandler(s.incidents.getIncident), + ShareRequestIncidentM3U: s.incidents.getCallsM3U, + ShareRequestTalkgroups: s.tgs.getTGsShareRoute, + } +} + func (rt ShareRequestType) IsValid() bool { switch rt { case ShareRequestCall, ShareRequestCallInfo, ShareRequestCallDL, ShareRequestIncident, @@ -94,6 +106,7 @@ func respondShareHandler(ie EntityFunc) ShareHandlerFunc { } } + func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI { return &shareAPI{ baseURL: baseURL, -- 2.48.1 From f6ea4a47538ddc9dac452630e2a357ad1d75cb07 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 1 Feb 2025 21:26:32 -0500 Subject: [PATCH 14/58] gofmt --- pkg/rest/share.go | 13 ++++++------- pkg/shares/store.go | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/rest/share.go b/pkg/rest/share.go index ce1d704..2a6633e 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -34,12 +34,12 @@ const ( // shareHandlers returns a ShareHandlers map from the api. func (s *api) shareHandlers() ShareHandlers { return ShareHandlers{ - ShareRequestCall: s.calls.shareCallRoute, - ShareRequestCallInfo: respondShareHandler(s.calls.getCallInfo), - ShareRequestCallDL: s.calls.shareCallDLRoute, - ShareRequestIncident: respondShareHandler(s.incidents.getIncident), - ShareRequestIncidentM3U: s.incidents.getCallsM3U, - ShareRequestTalkgroups: s.tgs.getTGsShareRoute, + ShareRequestCall: s.calls.shareCallRoute, + ShareRequestCallInfo: respondShareHandler(s.calls.getCallInfo), + ShareRequestCallDL: s.calls.shareCallDLRoute, + ShareRequestIncident: respondShareHandler(s.incidents.getIncident), + ShareRequestIncidentM3U: s.incidents.getCallsM3U, + ShareRequestTalkgroups: s.tgs.getTGsShareRoute, } } @@ -106,7 +106,6 @@ func respondShareHandler(ie EntityFunc) ShareHandlerFunc { } } - func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI { return &shareAPI{ baseURL: baseURL, diff --git a/pkg/shares/store.go b/pkg/shares/store.go index cf4e0d6..11e7e2f 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -85,7 +85,7 @@ func (s *postgresStore) Delete(ctx context.Context, id string) error { return err } - _, err = rbac.Check(ctx, sh, rbac.WithActions(entities.ActionDelete)) + _, err = rbac.Check(ctx, sh, rbac.WithActions(entities.ActionDelete)) if err != nil { return err } -- 2.48.1 From 3016de67f35f390734096334fe4e63f63d7f28da Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 2 Feb 2025 11:15:43 -0500 Subject: [PATCH 15/58] rename item --- pkg/rest/share.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 2a6633e..d57b442 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -68,9 +68,9 @@ type SharedItem interface { } type shareResponse struct { - ID ID `json:"id"` - Type shares.EntityType `json:"type"` - Item SharedItem `json:"item,omitempty"` + ID ID `json:"id"` + Type shares.EntityType `json:"type"` + SharedItem SharedItem `json:"sharedItem,omitempty"` } func ShareFrom(ctx context.Context) *shares.Share { @@ -97,9 +97,9 @@ func respondShareHandler(ie EntityFunc) ShareHandlerFunc { } sRes := shareResponse{ - ID: id, - Type: share.Type, - Item: res, + ID: id, + Type: share.Type, + SharedItem: res, } respond(w, r, sRes) -- 2.48.1 From 24c66e7c8779046a3480e12522b70f62f41e1f91 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 2 Feb 2025 11:32:28 -0500 Subject: [PATCH 16/58] Emit length in m3u --- pkg/calls/call.go | 7 +++++++ pkg/rest/incidents.go | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index 3e7ab4c..e523031 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -22,6 +22,13 @@ func (d CallDuration) Duration() time.Duration { return time.Duration(d) } +func (d CallDuration) ColonFormat() string { + dur := d.Duration().Round(time.Second) + m := dur / time.Minute + s := dur / time.Second + return fmt.Sprintf("%d:%02d", m, s) +} + func (d CallDuration) MsInt32Ptr() *int32 { if time.Duration(d) == 0 { return nil diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index ef52255..36a018a 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -235,11 +235,12 @@ func (ia *incidentsAPI) getCallsM3U(id ID, w http.ResponseWriter, r *http.Reques callUrl.Path = urlRoot + c.ID.String() - fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s)\n%s\n\n", + fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s @ %s)\n%s\n\n", c.Duration.Seconds(), tg.StringTag(true), from, - c.DateTime.Format("15:04 01/02"), + c.Duration.ColonFormat(), + c.DateTime.Format("15:04:05 01/02"), callUrl, ) } -- 2.48.1 From 0adf3b49786bd5dbc78649193f529e195995a3a6 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 2 Feb 2025 11:48:44 -0500 Subject: [PATCH 17/58] comment --- pkg/talkgroups/tgstore/store.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/talkgroups/tgstore/store.go b/pkg/talkgroups/tgstore/store.go index ba86f06..b7dd472 100644 --- a/pkg/talkgroups/tgstore/store.go +++ b/pkg/talkgroups/tgstore/store.go @@ -588,6 +588,7 @@ func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupPara } func (t *cache) DeleteSystem(ctx context.Context, id int) error { + // talkgroups don't have owners, so we can use a generic Resource _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete)) if err != nil { return err -- 2.48.1 From 3f4fe56ed06afcb3e9649a3b57628e9e9c608b88 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 2 Feb 2025 21:08:43 -0500 Subject: [PATCH 18/58] add audioURL to shares --- pkg/calls/call.go | 7 +++++++ pkg/incidents/incident.go | 10 ++++++++++ pkg/rest/share.go | 9 ++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index e523031..d3a4bc3 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -3,9 +3,11 @@ package calls import ( "encoding/json" "fmt" + "net/url" "time" "dynatron.me/x/stillbox/internal/audio" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/pb" "dynatron.me/x/stillbox/pkg/rbac/entities" @@ -94,6 +96,11 @@ func (c *Call) ShouldStore() bool { return c.shouldStore } +func (c *Call) SetShareURL(baseURL url.URL, shareID string) { + baseURL.Path = fmt.Sprintf("/share/%s/call", shareID) + c.AudioURL = common.PtrTo(baseURL.String()) +} + func Make(call *Call, dontStore bool) (*Call, error) { err := call.computeLength() if err != nil { diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index 0cc378b..9477454 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -2,8 +2,11 @@ package incidents import ( "encoding/json" + "fmt" + "net/url" "strings" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/rbac/entities" @@ -23,6 +26,13 @@ type Incident struct { Calls []IncidentCall `json:"calls"` } +func (inc *Incident) SetShareURL(bu url.URL, shareID string) { + bu.Path = fmt.Sprintf("/share/%s/call/", shareID) + for i := range inc.Calls { + inc.Calls[i].AudioURL = common.PtrTo(bu.String() + inc.Calls[i].ID.String()) + } +} + func (inc *Incident) GetResourceName() string { return entities.ResourceIncident } diff --git a/pkg/rest/share.go b/pkg/rest/share.go index d57b442..b0846da 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -35,9 +35,9 @@ const ( func (s *api) shareHandlers() ShareHandlers { return ShareHandlers{ ShareRequestCall: s.calls.shareCallRoute, - ShareRequestCallInfo: respondShareHandler(s.calls.getCallInfo), + ShareRequestCallInfo: s.respondShareHandler(s.calls.getCallInfo), ShareRequestCallDL: s.calls.shareCallDLRoute, - ShareRequestIncident: respondShareHandler(s.incidents.getIncident), + ShareRequestIncident: s.respondShareHandler(s.incidents.getIncident), ShareRequestIncidentM3U: s.incidents.getCallsM3U, ShareRequestTalkgroups: s.tgs.getTGsShareRoute, } @@ -65,6 +65,7 @@ type shareAPI struct { type EntityFunc func(ctx context.Context, id ID) (SharedItem, error) type SharedItem interface { + SetShareURL(baseURL url.URL, shareID string) } type shareResponse struct { @@ -81,7 +82,7 @@ func ShareFrom(ctx context.Context) *shares.Share { return nil } -func respondShareHandler(ie EntityFunc) ShareHandlerFunc { +func (s *api) respondShareHandler(ie EntityFunc) ShareHandlerFunc { return func(id ID, w http.ResponseWriter, r *http.Request) { ctx := r.Context() share := ShareFrom(ctx) @@ -102,6 +103,8 @@ func respondShareHandler(ie EntityFunc) ShareHandlerFunc { SharedItem: res, } + sRes.SharedItem.SetShareURL(*s.baseURL, share.ID) + respond(w, r, sRes) } } -- 2.48.1 From 632607cd0b426c3f8768df491a13878ae7014194 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 2 Feb 2025 21:17:13 -0500 Subject: [PATCH 19/58] Grab and store URL, if has --- pkg/calls/call.go | 4 ++++ pkg/calls/callstore/store.go | 1 + pkg/incidents/incident.go | 3 +++ pkg/incidents/incstore/store.go | 1 + 4 files changed, 9 insertions(+) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index d3a4bc3..aea54e1 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -97,6 +97,10 @@ func (c *Call) ShouldStore() bool { } func (c *Call) SetShareURL(baseURL url.URL, shareID string) { + if c.AudioURL != nil { + return + } + baseURL.Path = fmt.Sprintf("/share/%s/call", shareID) c.AudioURL = common.PtrTo(baseURL.String()) } diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 633b166..37b9b22 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -74,6 +74,7 @@ func toAddCallParams(call *calls.Call) database.AddCallParams { AudioName: common.NilIfZero(call.AudioName), AudioBlob: call.Audio, AudioType: common.NilIfZero(call.AudioType), + AudioUrl: call.AudioURL, Duration: call.Duration.MsInt32Ptr(), Frequency: call.Frequency, Frequencies: call.Frequencies, diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index 9477454..e8d2f7a 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -29,6 +29,9 @@ type Incident struct { func (inc *Incident) SetShareURL(bu url.URL, shareID string) { bu.Path = fmt.Sprintf("/share/%s/call/", shareID) for i := range inc.Calls { + if inc.Calls[i].AudioURL != nil { + continue + } inc.Calls[i].AudioURL = common.PtrTo(bu.String() + inc.Calls[i].ID.String()) } } diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 8aa284b..47ad24b 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -268,6 +268,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall { ID: v.CallID, AudioName: common.ZeroIfNil(v.AudioName), AudioType: common.ZeroIfNil(v.AudioType), + AudioURL: v.AudioUrl, Duration: dur, DateTime: v.CallDate.Time, Frequencies: v.Frequencies, -- 2.48.1 From e073f4044937075e5bba601e5f7fdc71974bf6b2 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 08:51:29 -0500 Subject: [PATCH 20/58] xscript --- pkg/calls/call.go | 1 + pkg/calls/callstore/store.go | 1 + pkg/database/calls.sql.go | 5 ++++- sql/postgres/queries/calls.sql | 3 ++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index aea54e1..da400c3 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -80,6 +80,7 @@ type Call struct { TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"` TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"` TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"` + Transcript *string `json:"transcript" relayOut:"transcript,omitempty"` shouldStore bool `json:"-"` } diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 37b9b22..297ddbb 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -179,6 +179,7 @@ func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) { TalkgroupLabel: c.TGLabel, TalkgroupGroup: c.TGGroup, TGAlphaTag: c.TGAlphaTag, + Transcript: c.Transcript, }, nil } diff --git a/pkg/database/calls.sql.go b/pkg/database/calls.sql.go index f86c2df..f614d0d 100644 --- a/pkg/database/calls.sql.go +++ b/pkg/database/calls.sql.go @@ -181,7 +181,8 @@ SELECT tg_label, tg_alpha_tag, tg_group, - source + source, + transcript FROM calls WHERE id = $1 ` @@ -203,6 +204,7 @@ type GetCallRow struct { TGAlphaTag *string `json:"tg_alpha_tag"` TGGroup *string `json:"tg_group"` Source int `json:"source"` + Transcript *string `json:"transcript"` } func (q *Queries) GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error) { @@ -225,6 +227,7 @@ func (q *Queries) GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error) &i.TGAlphaTag, &i.TGGroup, &i.Source, + &i.Transcript, ) return i, err } diff --git a/sql/postgres/queries/calls.sql b/sql/postgres/queries/calls.sql index a35ded6..8496475 100644 --- a/sql/postgres/queries/calls.sql +++ b/sql/postgres/queries/calls.sql @@ -180,6 +180,7 @@ SELECT tg_label, tg_alpha_tag, tg_group, - source + source, + transcript FROM calls WHERE id = @id; -- 2.48.1 From f3e8f7afd98152de680c82fbc128b796a655fddb Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 09:25:07 -0500 Subject: [PATCH 21/58] shares start public --- pkg/rbac/policy/policy.go | 10 ++++------ pkg/shares/store.go | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/rbac/policy/policy.go b/pkg/rbac/policy/policy.go index 16addcc..6cf50c1 100644 --- a/pkg/rbac/policy/policy.go +++ b/pkg/rbac/policy/policy.go @@ -112,13 +112,11 @@ var Policy = &restrict.PolicyDefinition{ Parents: []string{entities.RoleAdmin}, }, entities.RolePublic: { - /* - Grants: restrict.GrantsMap{ - entities.ResourceShare: { - &restrict.Permission{Action: entities.ActionRead}, - }, + Grants: restrict.GrantsMap{ + entities.ResourceShare: { + &restrict.Permission{Action: entities.ActionRead}, }, - */ + }, }, }, PermissionPresets: restrict.PermissionPresets{ diff --git a/pkg/shares/store.go b/pkg/shares/store.go index 11e7e2f..14287a7 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -48,6 +48,11 @@ func recToShare(share database.Share) *Share { } func (s *postgresStore) GetShare(ctx context.Context, id string) (*Share, error) { + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceShare), rbac.WithActions(entities.ActionRead)) + if err != nil { + return nil, err + } + db := database.FromCtx(ctx) rec, err := db.GetShare(ctx, id) switch err { -- 2.48.1 From 5329091236ff754fd3bc540cd9948b1096b6aa24 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 09:52:09 -0500 Subject: [PATCH 22/58] Fix cookie port error --- pkg/auth/jwt.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 896918c..fc20edf 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -211,7 +211,7 @@ func (a *Auth) routeRefresh(w http.ResponseWriter, r *http.Request) { } if cookie.Secure { - cookie.Domain = r.Host + cookie.Domain = strings.Split(r.Host, ":")[0] } http.SetCookie(w, cookie) @@ -271,7 +271,7 @@ func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) { MaxAge: 60 * 60 * 24 * 30, // one month } - cookie.Domain = r.Host + cookie.Domain = strings.Split(r.Host, ":")[0] if a.allowInsecureCookie(r) { a.setInsecureCookie(cookie) } @@ -297,7 +297,7 @@ func (a *Auth) routeLogout(w http.ResponseWriter, r *http.Request) { MaxAge: -1, } - cookie.Domain = r.Host + cookie.Domain = strings.Split(r.Host, ":")[0] if a.allowInsecureCookie(r) { cookie.Secure = true cookie.SameSite = http.SameSiteNoneMode -- 2.48.1 From 4600cc06601bead87be27179d46547f6d0872bc8 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 31 Jan 2025 09:03:46 -0500 Subject: [PATCH 23/58] broken --- .../incidents/incident/incident.component.ts | 21 +++++++++++++++---- .../src/app/share/share.component.html | 11 +++++++++- .../stillbox/src/app/share/share.component.ts | 21 +++++++++++++++++-- .../stillbox/src/app/share/share.service.ts | 12 +++++++---- client/stillbox/src/proxy.conf.json | 4 ++++ 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index ad5cdcc..cd1d21a 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,5 +1,5 @@ -import { Component, inject, Sanitizer } from '@angular/core'; -import { tap } from 'rxjs/operators'; +import { Component, inject, Input, input, Sanitizer } from '@angular/core'; +import { switchMap, tap } from 'rxjs/operators'; import { CommonModule, Location } from '@angular/common'; import { BehaviorSubject, merge, Subscription } from 'rxjs'; import { Observable } from 'rxjs'; @@ -39,6 +39,8 @@ import { import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component'; import { FmtDatePipe } from '../incidents.component'; import { MatMenuModule } from '@angular/material/menu'; +import { Share } from '../../share/share.service'; +import { toObservable } from '@angular/core/rxjs-interop'; export interface EditDialogData { incID: string; @@ -153,6 +155,7 @@ export class IncidentEditDialogComponent { export class IncidentComponent { incPrime = new BehaviorSubject({}); inc$!: Observable; + @Input() incident?: Share; subscriptions: Subscription = new Subscription(); dialog = inject(MatDialog); incID!: string; @@ -179,8 +182,18 @@ export class IncidentComponent { saveIncName(ev: Event) {} ngOnInit() { - this.incID = this.route.snapshot.paramMap.get('id')!; - this.inc$ = merge(this.incSvc.getIncident(this.incID), this.incPrime).pipe( + let incOb: Observable; + if (this.route.component === this.constructor) { // loaded by route + this.incID = this.route.snapshot.paramMap.get('id')!; + incOb = this.incSvc.getIncident(this.incID); + } else { + if (!this.incident) { + return; + } + this.incID = (this.incident.share as IncidentRecord).id; + incOb = new BehaviorSubject(this.incident.share as IncidentRecord); + } + this.inc$ = merge(incOb, this.incPrime).pipe( tap((inc) => { if (inc.calls) { this.callsResult.data = inc.calls; diff --git a/client/stillbox/src/app/share/share.component.html b/client/stillbox/src/app/share/share.component.html index 5ca71b6..8a7eaf6 100644 --- a/client/stillbox/src/app/share/share.component.html +++ b/client/stillbox/src/app/share/share.component.html @@ -1 +1,10 @@ -

share works!

+@let sh = share | async; +@if (sh == null) { + +} @else if (sh.shareType == 'incident') { + +} @else if (sh.shareType == 'call') { + +} @else { + +} \ No newline at end of file diff --git a/client/stillbox/src/app/share/share.component.ts b/client/stillbox/src/app/share/share.component.ts index 2f28cfc..127f62e 100644 --- a/client/stillbox/src/app/share/share.component.ts +++ b/client/stillbox/src/app/share/share.component.ts @@ -1,20 +1,37 @@ import { Component } from '@angular/core'; -import { ShareService } from './share.service'; +import { Share, ShareService } from './share.service'; import { ActivatedRoute } from '@angular/router'; +import { Observable, Subscription, switchMap } from 'rxjs'; +import { IncidentComponent } from '../incidents/incident/incident.component'; +import { AsyncPipe } from '@angular/common'; + @Component({ selector: 'app-share', - imports: [], + imports: [ + AsyncPipe, + IncidentComponent, + ], templateUrl: './share.component.html', styleUrl: './share.component.scss' }) export class ShareComponent { + shareID!: string; + share!: Observable; constructor( private route: ActivatedRoute, private shareSvc: ShareService, ) {} ngOnInit() { + let shareParam = this.route.snapshot.paramMap.get('id'); + if (shareParam == null) { + // TODO: error + return; + } + this.shareID = shareParam; + + this.share = this.shareSvc.getShare(this.shareID); } } diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index 4ea55f0..82433dd 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -3,7 +3,11 @@ import { Injectable } from '@angular/core'; import { map, Observable, switchMap } from 'rxjs'; import { IncidentRecord } from '../incidents'; -type Share = IncidentRecord | ArrayBuffer; +type ShareType = IncidentRecord | ArrayBuffer; +export interface Share { + shareType: string; + share: ShareType; +} @Injectable({ providedIn: 'root' }) @@ -14,14 +18,14 @@ export class ShareService { ) { } getShare(id: string): Observable { - return this.http.get(`/share/${id}`, {observe: 'response'}).pipe( + return this.http.get(`/share/${id}`, {observe: 'response'}).pipe( map((res) => { let typ = res.headers.get('X-Share-Type'); switch(typ) { case 'call': - return (res.body as ArrayBuffer); + return {shareType: typ, share: (res.body as ArrayBuffer)}; case 'incident': - return (res.body as IncidentRecord); + return {shareType: typ, share: (res.body as IncidentRecord)}; } return null; }) diff --git a/client/stillbox/src/proxy.conf.json b/client/stillbox/src/proxy.conf.json index 96af567..67915a8 100644 --- a/client/stillbox/src/proxy.conf.json +++ b/client/stillbox/src/proxy.conf.json @@ -2,5 +2,9 @@ "/api": { "target": "http://xenon:3050", "secure": false + }, + "/share": { + "target": "http://xenon:3050", + "secure": false } } -- 2.48.1 From 3a4fe1d9e886cd7e4de2731a6e871ca2107ebd2d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 1 Feb 2025 08:52:03 -0500 Subject: [PATCH 24/58] Works kind of, wip --- client/stillbox/ngsw-config.json | 3 +- .../stillbox/src/app/calls/calls.component.ts | 113 ++---------------- .../stillbox/src/app/calls/calls.service.ts | 94 ++++++++++++++- .../incidents/incident/incident.component.ts | 5 +- .../src/app/share/share.component.html | 12 +- .../src/app/share/share.component.spec.ts | 5 +- .../stillbox/src/app/share/share.component.ts | 10 +- .../stillbox/src/app/share/share.service.ts | 24 ++-- .../src/app/talkgroups/talkgroups.service.ts | 5 +- 9 files changed, 135 insertions(+), 136 deletions(-) diff --git a/client/stillbox/ngsw-config.json b/client/stillbox/ngsw-config.json index fa63476..0a011be 100644 --- a/client/stillbox/ngsw-config.json +++ b/client/stillbox/ngsw-config.json @@ -27,8 +27,7 @@ } } ], - "navigationUrls": - [ + "navigationUrls": [ "/**", "!/**/*.*", "!/**/****", diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index c82e9f4..573dab8 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -1,10 +1,4 @@ -import { - Component, - inject, - Pipe, - PipeTransform, - ViewChild, -} from '@angular/core'; +import { Component, inject, ViewChild } from '@angular/core'; import { CommonModule, AsyncPipe } from '@angular/common'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTableModule } from '@angular/material/table'; @@ -17,13 +11,20 @@ import { PrefsService } from '../prefs/prefs.service'; import { MatIconModule } from '@angular/material/icon'; import { SelectionModel } from '@angular/cdk/collections'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; -import { CallsListParams, CallsService } from './calls.service'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { + CallsListParams, + CallsService, + DatePipe, + DownloadURLPipe, + FixedPointPipe, + TalkgroupPipe, + TimePipe, +} from './calls.service'; import { CallRecord } from '../calls'; import { TalkgroupService } from '../talkgroups/talkgroups.service'; -import { Talkgroup } from '../talkgroup'; import { MatFormFieldModule } from '@angular/material/form-field'; import { FormControl, @@ -49,94 +50,6 @@ import { import { IncidentRecord } from '../incidents'; import { SelectIncidentDialogComponent } from '../incidents/select-incident-dialog/select-incident-dialog.component'; -@Pipe({ - name: 'grabDate', - standalone: true, - pure: true, -}) -export class DatePipe implements PipeTransform { - transform(ts: string, args?: any): string { - const timestamp = new Date(ts); - return timestamp.getMonth() + 1 + '/' + timestamp.getDate(); - } -} - -@Pipe({ - name: 'time', - standalone: true, - pure: true, -}) -export class TimePipe implements PipeTransform { - transform(ts: string, args?: any): string { - const timestamp = new Date(ts); - return timestamp.toLocaleTimeString(navigator.language, { - hour: '2-digit', - minute: '2-digit', - hourCycle: 'h23', - }); - } -} - -@Pipe({ - name: 'talkgroup', - standalone: true, - pure: true, -}) -export class TalkgroupPipe implements PipeTransform { - constructor(private tgService: TalkgroupService) {} - - transform(call: CallRecord, field: string): Observable { - return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe( - map((tg: Talkgroup) => { - switch (field) { - case 'alpha': { - return tg.alpha_tag ?? call.tgid; - break; - } - case 'group': { - return tg.tg_group ?? '\u2014'; - break; - } - case 'system': { - return tg.system?.name ?? tg.system_id.toString(); - } - default: { - return tg.name ?? '\u2014'; - break; - } - } - }), - ); - } -} - -@Pipe({ - name: 'fixedPoint', - standalone: true, - pure: true, -}) -export class FixedPointPipe implements PipeTransform { - constructor() {} - - transform(quant: number, divisor: number, places: number): string { - const seconds = quant / divisor; - return seconds.toFixed(places); - } -} - -@Pipe({ - name: 'audioDownloadURL', - standalone: true, - pure: true, -}) -export class DownloadURLPipe implements PipeTransform { - constructor(private callsSvc: CallsService) {} - - transform(call: CallRecord, args?: any): string { - return this.callsSvc.callAudioDownloadURL(call.id); - } -} - const reqPageSize = 200; @Component({ selector: 'app-calls', @@ -144,8 +57,8 @@ const reqPageSize = 200; MatIconModule, FixedPointPipe, TalkgroupPipe, - DatePipe, TimePipe, + DatePipe, MatPaginatorModule, MatTableModule, AsyncPipe, diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index 2792b9e..89cd8fc 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -1,8 +1,98 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Pipe, PipeTransform } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { CallRecord } from '../calls'; import { environment } from '.././../environments/environment'; +import { TalkgroupService } from '../talkgroups/talkgroups.service'; +import { Talkgroup } from '../talkgroup'; + +@Pipe({ + name: 'grabDate', + standalone: true, + pure: true, +}) +export class DatePipe implements PipeTransform { + transform(ts: string, args?: any): string { + const timestamp = new Date(ts); + return timestamp.getMonth() + 1 + '/' + timestamp.getDate(); + } +} + +@Pipe({ + name: 'time', + standalone: true, + pure: true, +}) +export class TimePipe implements PipeTransform { + transform(ts: string, args?: any): string { + const timestamp = new Date(ts); + return timestamp.toLocaleTimeString(navigator.language, { + hour: '2-digit', + minute: '2-digit', + hourCycle: 'h23', + }); + } +} + +@Pipe({ + name: 'talkgroup', + standalone: true, + pure: true, +}) +export class TalkgroupPipe implements PipeTransform { + constructor(private tgService: TalkgroupService) {} + + transform(call: CallRecord, field: string): Observable { + return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe( + map((tg: Talkgroup) => { + switch (field) { + case 'alpha': { + return tg.alpha_tag ?? call.tgid; + break; + } + case 'group': { + return tg.tg_group ?? '\u2014'; + break; + } + case 'system': { + return tg.system?.name ?? tg.system_id.toString(); + } + default: { + return tg.name ?? '\u2014'; + break; + } + } + }), + ); + } +} + +@Pipe({ + name: 'fixedPoint', + standalone: true, + pure: true, +}) +export class FixedPointPipe implements PipeTransform { + constructor() {} + + transform(quant: number, divisor: number, places: number): string { + const seconds = quant / divisor; + return seconds.toFixed(places); + } +} + +@Pipe({ + name: 'audioDownloadURL', + standalone: true, + pure: true, +}) +export class DownloadURLPipe implements PipeTransform { + constructor(private callsSvc: CallsService) {} + + transform(call: CallRecord, args?: any): string { + return this.callsSvc.callAudioDownloadURL(call.id); + } +} export interface CallsListParams { start: Date | null; diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index cd1d21a..d2b13b9 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -35,7 +35,7 @@ import { TimePipe, DatePipe, DownloadURLPipe, -} from '../../calls/calls.component'; +} from '../../calls/calls.service'; import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component'; import { FmtDatePipe } from '../incidents.component'; import { MatMenuModule } from '@angular/material/menu'; @@ -183,7 +183,8 @@ export class IncidentComponent { ngOnInit() { let incOb: Observable; - if (this.route.component === this.constructor) { // loaded by route + if (this.route.component === this.constructor) { + // loaded by route this.incID = this.route.snapshot.paramMap.get('id')!; incOb = this.incSvc.getIncident(this.incID); } else { diff --git a/client/stillbox/src/app/share/share.component.html b/client/stillbox/src/app/share/share.component.html index 8a7eaf6..7c670e2 100644 --- a/client/stillbox/src/app/share/share.component.html +++ b/client/stillbox/src/app/share/share.component.html @@ -1,10 +1,6 @@ @let sh = share | async; @if (sh == null) { - -} @else if (sh.shareType == 'incident') { - -} @else if (sh.shareType == 'call') { - -} @else { - -} \ No newline at end of file +} @else if (sh.shareType == "incident") { + +} @else if (sh.shareType == "call") { +} @else {} diff --git a/client/stillbox/src/app/share/share.component.spec.ts b/client/stillbox/src/app/share/share.component.spec.ts index 6115935..7a7fe1e 100644 --- a/client/stillbox/src/app/share/share.component.spec.ts +++ b/client/stillbox/src/app/share/share.component.spec.ts @@ -8,9 +8,8 @@ describe('ShareComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ShareComponent] - }) - .compileComponents(); + imports: [ShareComponent], + }).compileComponents(); fixture = TestBed.createComponent(ShareComponent); component = fixture.componentInstance; diff --git a/client/stillbox/src/app/share/share.component.ts b/client/stillbox/src/app/share/share.component.ts index 127f62e..e1557c9 100644 --- a/client/stillbox/src/app/share/share.component.ts +++ b/client/stillbox/src/app/share/share.component.ts @@ -5,19 +5,15 @@ import { Observable, Subscription, switchMap } from 'rxjs'; import { IncidentComponent } from '../incidents/incident/incident.component'; import { AsyncPipe } from '@angular/common'; - @Component({ selector: 'app-share', - imports: [ - AsyncPipe, - IncidentComponent, - ], + imports: [AsyncPipe, IncidentComponent], templateUrl: './share.component.html', - styleUrl: './share.component.scss' + styleUrl: './share.component.scss', }) export class ShareComponent { shareID!: string; - share!: Observable; + share!: Observable; constructor( private route: ActivatedRoute, private shareSvc: ShareService, diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index 82433dd..f880ab8 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -9,26 +9,28 @@ export interface Share { share: ShareType; } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ShareService { + constructor(private http: HttpClient) {} - constructor( - private http: HttpClient, - ) { } - - getShare(id: string): Observable { - return this.http.get(`/share/${id}`, {observe: 'response'}).pipe( + getShare(id: string): Observable { + return this.http + .get(`/share/${id}`, { observe: 'response' }) + .pipe( map((res) => { let typ = res.headers.get('X-Share-Type'); - switch(typ) { + switch (typ) { case 'call': - return {shareType: typ, share: (res.body as ArrayBuffer)}; + return { shareType: typ, share: res.body as ArrayBuffer }; case 'incident': - return {shareType: typ, share: (res.body as IncidentRecord)}; + return { + shareType: typ, + share: res.body as IncidentRecord, + }; } return null; - }) + }), ); } } diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 7a41161..0896b64 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -33,7 +33,10 @@ export class TalkgroupService { private subscriptions = new Subscription(); constructor(private http: HttpClient) { this.tgs$ = this.fetchAll.pipe(switchMap(() => this.getTalkgroups())); - this.tags$ = this.fetchAll.pipe(switchMap(() => this.getAllTags()), shareReplay()); + this.tags$ = this.fetchAll.pipe( + switchMap(() => this.getAllTags()), + shareReplay(), + ); this.fillTgMap(); } -- 2.48.1 From b262fbce0c878971a103fce7ae5c54b3e864d6b1 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 1 Feb 2025 14:28:39 -0500 Subject: [PATCH 25/58] wip --- .../src/app/incidents/incident/incident.component.ts | 11 +++++------ client/stillbox/src/app/share/share.component.html | 5 ++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index d2b13b9..1542e81 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,7 +1,7 @@ -import { Component, inject, Input, input, Sanitizer } from '@angular/core'; -import { switchMap, tap } from 'rxjs/operators'; +import { Component, inject, Input } from '@angular/core'; +import { tap } from 'rxjs/operators'; import { CommonModule, Location } from '@angular/common'; -import { BehaviorSubject, merge, Subscription } from 'rxjs'; +import { BehaviorSubject, merge, Subject, Subscription } from 'rxjs'; import { Observable } from 'rxjs'; import { ReactiveFormsModule, @@ -40,7 +40,6 @@ import { CallPlayerComponent } from '../../calls/player/call-player/call-player. import { FmtDatePipe } from '../incidents.component'; import { MatMenuModule } from '@angular/material/menu'; import { Share } from '../../share/share.service'; -import { toObservable } from '@angular/core/rxjs-interop'; export interface EditDialogData { incID: string; @@ -153,7 +152,7 @@ export class IncidentEditDialogComponent { styleUrl: './incident.component.scss', }) export class IncidentComponent { - incPrime = new BehaviorSubject({}); + incPrime = new Subject(); inc$!: Observable; @Input() incident?: Share; subscriptions: Subscription = new Subscription(); @@ -196,7 +195,7 @@ export class IncidentComponent { } this.inc$ = merge(incOb, this.incPrime).pipe( tap((inc) => { - if (inc.calls) { + if (inc && inc.calls) { this.callsResult.data = inc.calls; } }), diff --git a/client/stillbox/src/app/share/share.component.html b/client/stillbox/src/app/share/share.component.html index 7c670e2..6faac2b 100644 --- a/client/stillbox/src/app/share/share.component.html +++ b/client/stillbox/src/app/share/share.component.html @@ -1,6 +1,9 @@ @let sh = share | async; @if (sh == null) { +

Share invalid!

} @else if (sh.shareType == "incident") { } @else if (sh.shareType == "call") { -} @else {} +} @else { +

Share type {{sh.shareType}} unknown

+} -- 2.48.1 From cea770477881c179a80f45becdd1ae8eca78ac72 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 3 Feb 2025 08:10:54 -0500 Subject: [PATCH 26/58] improve share, tg --- client/stillbox/src/app/calls.ts | 1 + .../incidents/incident/incident.component.ts | 6 +-- .../src/app/share/share.component.html | 8 +-- .../stillbox/src/app/share/share.component.ts | 3 +- .../stillbox/src/app/share/share.service.ts | 50 ++++++++++--------- client/stillbox/src/app/shares.ts | 8 +++ .../src/app/talkgroups/talkgroups.service.ts | 10 ++-- 7 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 client/stillbox/src/app/shares.ts diff --git a/client/stillbox/src/app/calls.ts b/client/stillbox/src/app/calls.ts index ccd51ee..dddf93f 100644 --- a/client/stillbox/src/app/calls.ts +++ b/client/stillbox/src/app/calls.ts @@ -1,6 +1,7 @@ export interface CallRecord { id: string; call_date: Date; + audioURL: string | null; duration: number; system_id: number; tgid: number; diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 1542e81..e934bf4 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -39,7 +39,7 @@ import { import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component'; import { FmtDatePipe } from '../incidents.component'; import { MatMenuModule } from '@angular/material/menu'; -import { Share } from '../../share/share.service'; +import { Share } from '../../shares'; export interface EditDialogData { incID: string; @@ -190,8 +190,8 @@ export class IncidentComponent { if (!this.incident) { return; } - this.incID = (this.incident.share as IncidentRecord).id; - incOb = new BehaviorSubject(this.incident.share as IncidentRecord); + this.incID = (this.incident.sharedItem as IncidentRecord).id; + incOb = new BehaviorSubject(this.incident.sharedItem as IncidentRecord); } this.inc$ = merge(incOb, this.incPrime).pipe( tap((inc) => { diff --git a/client/stillbox/src/app/share/share.component.html b/client/stillbox/src/app/share/share.component.html index 6faac2b..7704aca 100644 --- a/client/stillbox/src/app/share/share.component.html +++ b/client/stillbox/src/app/share/share.component.html @@ -1,9 +1,9 @@ @let sh = share | async; @if (sh == null) { -

Share invalid!

-} @else if (sh.shareType == "incident") { +

Share invalid!

+} @else if (sh.type == "incident") { -} @else if (sh.shareType == "call") { +} @else if (sh.type == "call") { } @else { -

Share type {{sh.shareType}} unknown

+

Share type {{ sh.type }} unknown

} diff --git a/client/stillbox/src/app/share/share.component.ts b/client/stillbox/src/app/share/share.component.ts index e1557c9..90766d5 100644 --- a/client/stillbox/src/app/share/share.component.ts +++ b/client/stillbox/src/app/share/share.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; -import { Share, ShareService } from './share.service'; +import { ShareService } from './share.service'; +import { Share } from '../shares'; import { ActivatedRoute } from '@angular/router'; import { Observable, Subscription, switchMap } from 'rxjs'; import { IncidentComponent } from '../incidents/incident/incident.component'; diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index f880ab8..d13d118 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -2,35 +2,39 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { map, Observable, switchMap } from 'rxjs'; import { IncidentRecord } from '../incidents'; +import { CallRecord } from '../calls'; +import { Share, ShareType } from '../shares'; -type ShareType = IncidentRecord | ArrayBuffer; -export interface Share { - shareType: string; - share: ShareType; -} @Injectable({ providedIn: 'root', }) export class ShareService { constructor(private http: HttpClient) {} - getShare(id: string): Observable { - return this.http - .get(`/share/${id}`, { observe: 'response' }) - .pipe( - map((res) => { - let typ = res.headers.get('X-Share-Type'); - switch (typ) { - case 'call': - return { shareType: typ, share: res.body as ArrayBuffer }; - case 'incident': - return { - shareType: typ, - share: res.body as IncidentRecord, - }; - } - return null; - }), - ); + getShare(id: string): Observable { + return this.http.get(`/share/${id}`); + } + + getSharedItem(s: Observable): Observable { + return s.pipe( + map((res) => { + switch (res.type) { + case 'call': + return res.sharedItem; + case 'incident': + return res.sharedItem; + } + + return null; + }), + ); + } + + getCallAudio(s: Observable): Observable { + return s.pipe( + switchMap((res) => { + return this.http.get(res.audioURL!); + }), + ); } } diff --git a/client/stillbox/src/app/shares.ts b/client/stillbox/src/app/shares.ts new file mode 100644 index 0000000..8902f74 --- /dev/null +++ b/client/stillbox/src/app/shares.ts @@ -0,0 +1,8 @@ +import { IncidentRecord } from './incidents'; +import { CallRecord } from './calls'; +export type ShareType = IncidentRecord | CallRecord | null; +export interface Share { + id: string; + type: string; + sharedItem: ShareType; +} diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 0896b64..9025ec9 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { BehaviorSubject, - concatMap, Observable, ReplaySubject, shareReplay, @@ -32,7 +31,10 @@ export class TalkgroupService { private fetchAll = new BehaviorSubject<'fetch'>('fetch'); private subscriptions = new Subscription(); constructor(private http: HttpClient) { - this.tgs$ = this.fetchAll.pipe(switchMap(() => this.getTalkgroups())); + this.tgs$ = this.fetchAll.pipe( + switchMap(() => this.getTalkgroups()), + shareReplay(), + ); this.tags$ = this.fetchAll.pipe( switchMap(() => this.getAllTags()), shareReplay(), @@ -45,11 +47,11 @@ export class TalkgroupService { } getAllTags(): Observable { - return this.http.get('/api/talkgroup/tags').pipe(shareReplay()); + return this.http.get('/api/talkgroup/tags'); } getTalkgroups(): Observable { - return this.http.get('/api/talkgroup/').pipe(shareReplay()); + return this.http.get('/api/talkgroup/'); } getTalkgroup(sys: number, tg: number): Observable { -- 2.48.1 From 4d5dfb94cd4e0390efc318ea6b7bbdb2b55a4d70 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 3 Feb 2025 08:33:27 -0500 Subject: [PATCH 27/58] callurl --- .../app/calls/player/call-player/call-player.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts index 2b528bc..18ab151 100644 --- a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts +++ b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts @@ -30,7 +30,11 @@ export class CallPlayerComponent { this.playSub.unsubscribe(); }); this.playing = true; - this.au.src = this.callsSvc.callAudioURL(this.call.id); + if (this.call.audioURL != null) { + this.au.src = this.call.audioURL; + } else { + this.au.src = this.callsSvc.callAudioURL(this.call.id); + } this.au.load(); this.au.play().then(null, (reason) => { this.playing = false; -- 2.48.1 From ccd1830da92783e31c4c466d33a6e6f5062c735c Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 3 Feb 2025 09:03:26 -0500 Subject: [PATCH 28/58] talkgroup pipe share wip --- client/stillbox/src/app/calls/calls.service.ts | 3 ++- .../src/app/incidents/incident/incident.component.html | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index 89cd8fc..19e7bc8 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -5,6 +5,7 @@ import { CallRecord } from '../calls'; import { environment } from '.././../environments/environment'; import { TalkgroupService } from '../talkgroups/talkgroups.service'; import { Talkgroup } from '../talkgroup'; +import { Share } from '../shares'; @Pipe({ name: 'grabDate', @@ -42,7 +43,7 @@ export class TimePipe implements PipeTransform { export class TalkgroupPipe implements PipeTransform { constructor(private tgService: TalkgroupService) {} - transform(call: CallRecord, field: string): Observable { + transform(call: CallRecord, field: string, share: Share|null = null): Observable { return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe( map((tg: Talkgroup) => { switch (field) { diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index e9b8612..0e07b87 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -76,13 +76,13 @@ System - {{ call | talkgroup: "system" | async }} + {{ call | talkgroup: "system":incident | async }} Group - {{ call | talkgroup: "group" | async }} + {{ call | talkgroup: "group":incident | async }} -- 2.48.1 From d8a1f2b85e729f842fa6438bae3b76973a31d323 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 3 Feb 2025 21:34:08 -0500 Subject: [PATCH 29/58] html5 media --- .../calls/call-info/call-info.component.html | 7 +++ .../calls/call-info/call-info.component.scss | 0 .../call-info/call-info.component.spec.ts | 22 +++++++ .../calls/call-info/call-info.component.ts | 20 +++++++ .../stillbox/src/app/calls/calls.service.ts | 58 ++++++++++++++++++- .../incident/incident.component.html | 4 +- .../incidents/incident/incident.component.ts | 8 +-- .../src/app/share/share.component.html | 22 ++++--- .../stillbox/src/app/share/share.component.ts | 11 +++- .../src/app/talkgroups/talkgroups.service.ts | 7 ++- 10 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 client/stillbox/src/app/calls/call-info/call-info.component.html create mode 100644 client/stillbox/src/app/calls/call-info/call-info.component.scss create mode 100644 client/stillbox/src/app/calls/call-info/call-info.component.spec.ts create mode 100644 client/stillbox/src/app/calls/call-info/call-info.component.ts diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.html b/client/stillbox/src/app/calls/call-info/call-info.component.html new file mode 100644 index 0000000..e04c8ba --- /dev/null +++ b/client/stillbox/src/app/calls/call-info/call-info.component.html @@ -0,0 +1,7 @@ + +
+
Time
+
{{ call.call_date }}
+
+ +
diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.scss b/client/stillbox/src/app/calls/call-info/call-info.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.spec.ts b/client/stillbox/src/app/calls/call-info/call-info.component.spec.ts new file mode 100644 index 0000000..88fbfaa --- /dev/null +++ b/client/stillbox/src/app/calls/call-info/call-info.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CallInfoComponent } from './call-info.component'; + +describe('CallInfoComponent', () => { + let component: CallInfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CallInfoComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CallInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.ts b/client/stillbox/src/app/calls/call-info/call-info.component.ts new file mode 100644 index 0000000..500591c --- /dev/null +++ b/client/stillbox/src/app/calls/call-info/call-info.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { CallRecord } from '../../calls'; +import { Share } from '../../shares'; +import { SafePipe } from '../calls.service'; + +@Component({ + selector: 'app-call-info', + imports: [MatCardModule, SafePipe], + templateUrl: './call-info.component.html', + styleUrl: './call-info.component.scss', +}) +export class CallInfoComponent { + @Input() share!: Share; + call!: CallRecord; + + ngOnInit() { + this.call = this.share.sharedItem as CallRecord; + } +} diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index 19e7bc8..ffe8e8c 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -6,6 +6,14 @@ import { environment } from '.././../environments/environment'; import { TalkgroupService } from '../talkgroups/talkgroups.service'; import { Talkgroup } from '../talkgroup'; import { Share } from '../shares'; +import { + DomSanitizer, + SafeHtml, + SafeResourceUrl, + SafeScript, + SafeStyle, + SafeUrl, +} from '@angular/platform-browser'; @Pipe({ name: 'grabDate', @@ -43,7 +51,11 @@ export class TimePipe implements PipeTransform { export class TalkgroupPipe implements PipeTransform { constructor(private tgService: TalkgroupService) {} - transform(call: CallRecord, field: string, share: Share|null = null): Observable { + transform( + call: CallRecord, + field: string, + share: Share | null = null, + ): Observable { return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe( map((tg: Talkgroup) => { switch (field) { @@ -82,6 +94,50 @@ export class FixedPointPipe implements PipeTransform { } } +/** + * Sanitize HTML + */ +@Pipe({ + name: 'safe', +}) +export class SafePipe implements PipeTransform { + /** + * Pipe Constructor + * + * @param _sanitizer: DomSanitezer + */ + // tslint:disable-next-line + constructor(protected _sanitizer: DomSanitizer) {} + + /** + * Transform + * + * @param value: string + * @param type: string + */ + transform( + value: string, + type: string, + ): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { + switch (type) { + case 'html': + return this._sanitizer.bypassSecurityTrustHtml(value); + case 'style': + return this._sanitizer.bypassSecurityTrustStyle(value); + case 'script': + return this._sanitizer.bypassSecurityTrustScript(value); + case 'url': + return this._sanitizer.bypassSecurityTrustUrl(value); + case 'resourceUrl': + let res = this._sanitizer.bypassSecurityTrustResourceUrl(value); + console.log(res); + return res; + default: + return this._sanitizer.bypassSecurityTrustHtml(value); + } + } +} + @Pipe({ name: 'audioDownloadURL', standalone: true, diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 0e07b87..e3cde1c 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -76,13 +76,13 @@ System - {{ call | talkgroup: "system":incident | async }} + {{ call | talkgroup: "system" : share | async }} Group - {{ call | talkgroup: "group":incident | async }} + {{ call | talkgroup: "group" : share | async }} diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index e934bf4..465e48a 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -154,7 +154,7 @@ export class IncidentEditDialogComponent { export class IncidentComponent { incPrime = new Subject(); inc$!: Observable; - @Input() incident?: Share; + @Input() share?: Share; subscriptions: Subscription = new Subscription(); dialog = inject(MatDialog); incID!: string; @@ -187,11 +187,11 @@ export class IncidentComponent { this.incID = this.route.snapshot.paramMap.get('id')!; incOb = this.incSvc.getIncident(this.incID); } else { - if (!this.incident) { + if (!this.share) { return; } - this.incID = (this.incident.sharedItem as IncidentRecord).id; - incOb = new BehaviorSubject(this.incident.sharedItem as IncidentRecord); + this.incID = (this.share.sharedItem as IncidentRecord).id; + incOb = new BehaviorSubject(this.share.sharedItem as IncidentRecord); } this.inc$ = merge(incOb, this.incPrime).pipe( tap((inc) => { diff --git a/client/stillbox/src/app/share/share.component.html b/client/stillbox/src/app/share/share.component.html index 7704aca..da8af0f 100644 --- a/client/stillbox/src/app/share/share.component.html +++ b/client/stillbox/src/app/share/share.component.html @@ -1,9 +1,17 @@ @let sh = share | async; -@if (sh == null) { -

Share invalid!

-} @else if (sh.type == "incident") { - -} @else if (sh.type == "call") { -} @else { -

Share type {{ sh.type }} unknown

+@switch (sh?.type) { + @case ("incident") { + + } + @case ("call") { + + } + @case (null) { +
+ +
+ } + @default { +

Share type {{ sh?.type }} unknown

+ } } diff --git a/client/stillbox/src/app/share/share.component.ts b/client/stillbox/src/app/share/share.component.ts index 90766d5..7c0a118 100644 --- a/client/stillbox/src/app/share/share.component.ts +++ b/client/stillbox/src/app/share/share.component.ts @@ -5,16 +5,23 @@ import { ActivatedRoute } from '@angular/router'; import { Observable, Subscription, switchMap } from 'rxjs'; import { IncidentComponent } from '../incidents/incident/incident.component'; import { AsyncPipe } from '@angular/common'; +import { CallInfoComponent } from '../calls/call-info/call-info.component'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @Component({ selector: 'app-share', - imports: [AsyncPipe, IncidentComponent], + imports: [ + AsyncPipe, + IncidentComponent, + CallInfoComponent, + MatProgressSpinnerModule, + ], templateUrl: './share.component.html', styleUrl: './share.component.scss', }) export class ShareComponent { shareID!: string; - share!: Observable; + share!: Observable; constructor( private route: ActivatedRoute, private shareSvc: ShareService, diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 9025ec9..bca0b81 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -9,6 +9,7 @@ import { switchMap, } from 'rxjs'; import { Talkgroup, TalkgroupUpdate, TGID } from '../talkgroup'; +import { Share } from '../shares'; export interface Pagination { page: number; @@ -54,7 +55,11 @@ export class TalkgroupService { return this.http.get('/api/talkgroup/'); } - getTalkgroup(sys: number, tg: number): Observable { + getTalkgroup( + sys: number, + tg: number, + share: Share | null = null, + ): Observable { const key = this.tgKey(sys, tg); if (!this._getTalkgroup.get(key)) { let rs = new ReplaySubject(); -- 2.48.1 From 3d39878198a3196946666d201b36a11bea7b4158 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 08:54:19 -0500 Subject: [PATCH 30/58] wip --- .../calls/call-info/call-info.component.html | 14 ++++- .../calls/call-info/call-info.component.scss | 63 +++++++++++++++++++ .../calls/call-info/call-info.component.ts | 12 +++- .../stillbox/src/app/calls/calls.service.ts | 5 +- .../incident/incident.component.scss | 8 +-- 5 files changed, 88 insertions(+), 14 deletions(-) diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.html b/client/stillbox/src/app/calls/call-info/call-info.component.html index e04c8ba..f15bf1a 100644 --- a/client/stillbox/src/app/calls/call-info/call-info.component.html +++ b/client/stillbox/src/app/calls/call-info/call-info.component.html @@ -1,7 +1,15 @@ -
-
Time
-
{{ call.call_date }}
+
+

+ {{ call | talkgroup: "alpha" | async }} @ + {{ call.call_date | grabDate }} {{ call.call_date | time: true }} +

+
+
+
Talkgroup
+
{{ call | talkgroup: "name" | async }}
+
Group
+
{{ call | talkgroup: "group" | async }}
diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.scss b/client/stillbox/src/app/calls/call-info/call-info.component.scss index e69de29..3cb56ff 100644 --- a/client/stillbox/src/app/calls/call-info/call-info.component.scss +++ b/client/stillbox/src/app/calls/call-info/call-info.component.scss @@ -0,0 +1,63 @@ +.callInfo { + margin: 0px 50px 50px 50px; + padding: 50px 50px 50px 50px; + display: flex; + flex-flow: column; + margin-left: auto; + margin-right: auto; +} + +.call-heading { + display: flex; + margin-bottom: 20px; + flex: 0 0; + flex-wrap: wrap; +} + +.cardHdr { + display: flex; + flex-flow: row wrap; + margin-bottom: 24px; +} + +.field { + flex: 1 0; + padding-left: 10px; + padding-right: 10px; +} + +.field-label { + font-weight: bolder; + flex-grow: 0; + padding-right: 5px; +} +.field-label::after { + content: ":"; +} + +.cardHdr h1 { + flex: 1 1; + margin: 0; +} + +.cardHdr a { + flex: 0 0; + justify-content: flex-end; + align-content: center; + cursor: pointer; +} + +form mat-form-field { + width: 60rem; + flex: 0 0; + display: flex; +} + +.callRecord { + display: flex; + flex-flow: column nowrap; + justify-content: center; + margin-top: 20px; + margin-left: auto; + margin-right: auto; +} diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.ts b/client/stillbox/src/app/calls/call-info/call-info.component.ts index 500591c..f8668fc 100644 --- a/client/stillbox/src/app/calls/call-info/call-info.component.ts +++ b/client/stillbox/src/app/calls/call-info/call-info.component.ts @@ -2,11 +2,19 @@ import { Component, Input } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { CallRecord } from '../../calls'; import { Share } from '../../shares'; -import { SafePipe } from '../calls.service'; +import { DatePipe, SafePipe, TalkgroupPipe, TimePipe } from '../calls.service'; +import { AsyncPipe } from '@angular/common'; @Component({ selector: 'app-call-info', - imports: [MatCardModule, SafePipe], + imports: [ + MatCardModule, + TimePipe, + DatePipe, + TalkgroupPipe, + AsyncPipe, + SafePipe, + ], templateUrl: './call-info.component.html', styleUrl: './call-info.component.scss', }) diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index ffe8e8c..1d6c8ee 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -21,7 +21,7 @@ import { pure: true, }) export class DatePipe implements PipeTransform { - transform(ts: string, args?: any): string { + transform(ts: string | Date, args?: any): string { const timestamp = new Date(ts); return timestamp.getMonth() + 1 + '/' + timestamp.getDate(); } @@ -33,11 +33,12 @@ export class DatePipe implements PipeTransform { pure: true, }) export class TimePipe implements PipeTransform { - transform(ts: string, args?: any): string { + transform(ts: string | Date, haveSecond: boolean = false): string { const timestamp = new Date(ts); return timestamp.toLocaleTimeString(navigator.language, { hour: '2-digit', minute: '2-digit', + second: haveSecond ? '2-digit' : undefined, hourCycle: 'h23', }); } diff --git a/client/stillbox/src/app/incidents/incident/incident.component.scss b/client/stillbox/src/app/incidents/incident/incident.component.scss index 08e0820..3ac603c 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.scss +++ b/client/stillbox/src/app/incidents/incident/incident.component.scss @@ -1,5 +1,5 @@ .incident { - margin: 50px 50px 50px 50px; + margin: 0px 50px 50px 50px; padding: 50px 50px 50px 50px; display: flex; flex-flow: column; @@ -7,12 +7,6 @@ margin-right: auto; } -@media not screen and (max-width: 768px) { - .incident { - width: 75%; - } -} - .inc-heading { display: flex; margin-bottom: 20px; -- 2.48.1 From b5eb5800b042c17510e5489ee24082d456d2c804 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 10:07:58 -0500 Subject: [PATCH 31/58] Password prompt --- cmd/calls/main.go | 31 +++++++++++++++++++++++++++++++ go.mod | 10 +++++----- go.sum | 23 ++++------------------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/cmd/calls/main.go b/cmd/calls/main.go index 9be38b3..3f73c2d 100644 --- a/cmd/calls/main.go +++ b/cmd/calls/main.go @@ -1,9 +1,11 @@ package main import ( + "bufio" "encoding/json" "errors" "flag" + "fmt" "log" "net/http" "net/http/cookiejar" @@ -11,10 +13,12 @@ import ( "os" "os/signal" "strings" + "syscall" "time" "dynatron.me/x/stillbox/internal/version" "dynatron.me/x/stillbox/pkg/pb" + "golang.org/x/term" "github.com/gorilla/websocket" "google.golang.org/protobuf/proto" @@ -37,6 +41,31 @@ func userAgent(h http.Header) { h.Set("User-Agent", uaString) } +func getCreds() { + rdr := bufio.NewReader(os.Stdin) + if username == nil || *username == "" { + fmt.Print("Username: ") + un, err := rdr.ReadString('\n') + if err != nil { + panic(err) + } + + username = &un + } + + if password == nil || *password == "" { + fmt.Print("Password: ") + bytePass, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + panic(err) + } + + pS := string(bytePass) + pS = strings.Trim(pS, "\n") + password = &pS + } +} + func main() { flag.Parse() log.SetFlags(0) @@ -53,6 +82,8 @@ func main() { signal.Notify(interrupt, os.Interrupt) play := NewPlayer() + getCreds() + loginForm := url.Values{} loginForm.Add("username", *username) loginForm.Add("password", *password) diff --git a/go.mod b/go.mod index a766b05..6491122 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.2 require ( dynatron.me/x/go-minimp3 v0.0.0-20240805171536-7ea857e216d6 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de + github.com/el-mike/restrict/v2 v2.0.0 github.com/go-audio/wav v1.1.0 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 @@ -21,6 +22,8 @@ require ( github.com/knadh/koanf/providers/env v1.0.0 github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/v2 v2.1.2 + github.com/lestrrat-go/jwx/v2 v2.1.3 + github.com/matoous/go-nanoid v1.5.1 github.com/nikoksr/notify v1.1.0 github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.10.0 @@ -28,7 +31,7 @@ require ( github.com/urfave/cli/v2 v2.27.5 golang.org/x/crypto v0.29.0 golang.org/x/sync v0.9.0 - golang.org/x/term v0.26.0 + golang.org/x/term v0.28.0 google.golang.org/protobuf v1.35.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -39,7 +42,6 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/el-mike/restrict/v2 v2.0.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-audio/audio v1.0.0 // indirect github.com/go-audio/riff v1.0.0 // indirect @@ -55,9 +57,7 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/matoous/go-nanoid v1.5.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -71,6 +71,6 @@ require ( golang.org/x/exp/shiny v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/image v0.22.0 // indirect golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.20.0 // indirect ) diff --git a/go.sum b/go.sum index 26398c9..4df4dff 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,6 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic= -github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= -github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= -github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -69,7 +63,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -117,8 +110,6 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc= -github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y= github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo= github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= @@ -176,7 +167,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -196,7 +186,6 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -208,24 +197,20 @@ golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4 golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f h1:23H/YlmTHfmmvpZ+ajKZL0qLz0+IwFOIqQA0mQbmLeM= golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f/go.mod h1:UbSUP4uu/C9hw9R2CkojhXlAxvayHjBdU9aRvE+c1To= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -- 2.48.1 From f14e50d258c07720838540b3835b94813f2a6419 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 10:08:15 -0500 Subject: [PATCH 32/58] share context --- .../src/app/incidents/incident/incident.component.html | 2 ++ .../src/app/incidents/incident/incident.component.ts | 6 +++++- client/stillbox/src/app/share/share.service.ts | 7 ++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index e3cde1c..0bd0390 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -7,6 +7,7 @@ >playlist_play + @if (!inShare) { @@ -18,6 +19,7 @@ Delete + }
Start
diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 465e48a..a92f01c 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -9,7 +9,7 @@ import { FormControl, FormsModule, } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -40,6 +40,7 @@ import { CallPlayerComponent } from '../../calls/player/call-player/call-player. import { FmtDatePipe } from '../incidents.component'; import { MatMenuModule } from '@angular/material/menu'; import { Share } from '../../shares'; +import { ShareService } from '../../share/share.service'; export interface EditDialogData { incID: string; @@ -152,6 +153,7 @@ export class IncidentEditDialogComponent { styleUrl: './incident.component.scss', }) export class IncidentComponent { + inShare = false; incPrime = new Subject(); inc$!: Observable; @Input() share?: Share; @@ -174,6 +176,7 @@ export class IncidentComponent { constructor( private route: ActivatedRoute, + private shareSvc: ShareService, private incSvc: IncidentsService, private location: Location, ) {} @@ -181,6 +184,7 @@ export class IncidentComponent { saveIncName(ev: Event) {} ngOnInit() { + this.inShare = this.shareSvc.isInShare(); let incOb: Observable; if (this.route.component === this.constructor) { // loaded by route diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index d13d118..9a1723d 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -4,12 +4,17 @@ import { map, Observable, switchMap } from 'rxjs'; import { IncidentRecord } from '../incidents'; import { CallRecord } from '../calls'; import { Share, ShareType } from '../shares'; +import { Router } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class ShareService { - constructor(private http: HttpClient) {} + constructor(private http: HttpClient, private router: Router) {} + + isInShare(): boolean { + return this.router.url.startsWith('/s/'); + } getShare(id: string): Observable { return this.http.get(`/share/${id}`); -- 2.48.1 From 04b0d0b5dee0c69833a413ee9ec24310b813853a Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 18:51:18 -0500 Subject: [PATCH 33/58] wip --- client/stillbox/src/app/app.routes.ts | 6 +++++ .../stillbox/src/app/calls/calls.component.ts | 11 +++++---- .../app/navigation/navigation.component.ts | 5 ++++ .../src/app/shares/shares.component.html | 1 + .../src/app/shares/shares.component.scss | 0 .../src/app/shares/shares.component.spec.ts | 23 +++++++++++++++++++ .../src/app/shares/shares.component.ts | 11 +++++++++ .../src/app/talkgroups/talkgroups.service.ts | 18 ++++++++------- 8 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 client/stillbox/src/app/shares/shares.component.html create mode 100644 client/stillbox/src/app/shares/shares.component.scss create mode 100644 client/stillbox/src/app/shares/shares.component.spec.ts create mode 100644 client/stillbox/src/app/shares/shares.component.ts diff --git a/client/stillbox/src/app/app.routes.ts b/client/stillbox/src/app/app.routes.ts index a904bb2..c5ba17d 100644 --- a/client/stillbox/src/app/app.routes.ts +++ b/client/stillbox/src/app/app.routes.ts @@ -76,6 +76,12 @@ export const routes: Routes = [ import('./alerts/alerts.component').then((m) => m.AlertsComponent), data: { title: 'Alerts' }, }, + { + path: 'shares', + loadComponent: () => + import('./shares/shares.component').then((m) => m.SharesComponent), + data: { title: 'Shares' }, + }, ], }, ]; diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 573dab8..9e03243 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -11,7 +11,7 @@ import { PrefsService } from '../prefs/prefs.service'; import { MatIconModule } from '@angular/material/icon'; import { SelectionModel } from '@angular/cdk/collections'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { CallsListParams, @@ -115,9 +115,7 @@ export class CallsComponent { subscriptions = new Subscription(); pageWindow = 0; - fetchCalls = new BehaviorSubject( - this.buildParams(this.curPage, this.curPage.pageIndex), - ); + fetchCalls = new Subject(); constructor( private callsSvc: CallsService, @@ -180,6 +178,7 @@ export class CallsComponent { } setPage(p: PageEvent, force?: boolean) { + console.log("setpage") this.selection.clear(); this.curPage = p; if (p && p!.pageSize != this.perPage) { @@ -195,16 +194,19 @@ export class CallsComponent { } getCalls(p: PageEvent, force?: boolean) { + console.log("getcalls") const pageStart = p.pageIndex * p.pageSize; const serverPage = Math.floor(pageStart / reqPageSize) + 1; this.pageWindow = pageStart % reqPageSize; if (serverPage == this.currentServerPage && !force && this.currentSet) { + console.log("currentset"); this.callsResult.next( this.callsResult ? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize) : [], ); } else { + console.log("not currentset"); this.currentServerPage = serverPage; this.fetchCalls.next(this.buildParams(p, serverPage)); } @@ -243,6 +245,7 @@ export class CallsComponent { this.fetchCalls .pipe( switchMap((params) => { + console.log("gc switchmap"); return this.callsSvc.getCalls(params); }), ) diff --git a/client/stillbox/src/app/navigation/navigation.component.ts b/client/stillbox/src/app/navigation/navigation.component.ts index 9c58f8a..8923bf9 100644 --- a/client/stillbox/src/app/navigation/navigation.component.ts +++ b/client/stillbox/src/app/navigation/navigation.component.ts @@ -101,6 +101,11 @@ export class NavigationComponent { url: '/alerts', icon: 'notifications', }, + { + name: 'Shares', + url: '/shares', + icon: 'share', + }, ]; toggleFilterPanel() { diff --git a/client/stillbox/src/app/shares/shares.component.html b/client/stillbox/src/app/shares/shares.component.html new file mode 100644 index 0000000..9f6f00b --- /dev/null +++ b/client/stillbox/src/app/shares/shares.component.html @@ -0,0 +1 @@ +

shares works!

diff --git a/client/stillbox/src/app/shares/shares.component.scss b/client/stillbox/src/app/shares/shares.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/stillbox/src/app/shares/shares.component.spec.ts b/client/stillbox/src/app/shares/shares.component.spec.ts new file mode 100644 index 0000000..a995779 --- /dev/null +++ b/client/stillbox/src/app/shares/shares.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SharesComponent } from './shares.component'; + +describe('SharesComponent', () => { + let component: SharesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SharesComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SharesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/stillbox/src/app/shares/shares.component.ts b/client/stillbox/src/app/shares/shares.component.ts new file mode 100644 index 0000000..dcd17d0 --- /dev/null +++ b/client/stillbox/src/app/shares/shares.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-shares', + imports: [], + templateUrl: './shares.component.html', + styleUrl: './shares.component.scss' +}) +export class SharesComponent { + +} diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index bca0b81..9eb25f8 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -27,20 +27,15 @@ export interface TalkgroupsPaginated { }) export class TalkgroupService { private readonly _getTalkgroup = new Map>(); - private tgs$: Observable; + private tgs$!: Observable; private tags$!: Observable; private fetchAll = new BehaviorSubject<'fetch'>('fetch'); private subscriptions = new Subscription(); constructor(private http: HttpClient) { - this.tgs$ = this.fetchAll.pipe( - switchMap(() => this.getTalkgroups()), - shareReplay(), - ); this.tags$ = this.fetchAll.pipe( switchMap(() => this.getAllTags()), shareReplay(), ); - this.fillTgMap(); } ngOnDestroy() { @@ -51,7 +46,7 @@ export class TalkgroupService { return this.http.get('/api/talkgroup/tags'); } - getTalkgroups(): Observable { + getTalkgroups(share: Share | null): Observable { return this.http.get('/api/talkgroup/'); } @@ -60,6 +55,9 @@ export class TalkgroupService { tg: number, share: Share | null = null, ): Observable { + if (this._getTalkgroup.size < 1) { + this.fillTgMap(share); + } const key = this.tgKey(sys, tg); if (!this._getTalkgroup.get(key)) { let rs = new ReplaySubject(); @@ -103,7 +101,11 @@ export class TalkgroupService { return this.http.post('/api/talkgroup/', pagination); } - fillTgMap() { + fillTgMap(share: Share | null) { + this.tgs$ = this.fetchAll.pipe( + switchMap(() => this.getTalkgroups(share)), + shareReplay(), + ); this.subscriptions.add( this.tgs$.subscribe((tgs) => { tgs.forEach((tg) => { -- 2.48.1 From 9b8da819c697e1b86831fc8a6173075577970233 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 21:06:40 -0500 Subject: [PATCH 34/58] bouncetime --- client/stillbox/src/app/calls/calls.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 9e03243..dceda59 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -115,7 +115,9 @@ export class CallsComponent { subscriptions = new Subscription(); pageWindow = 0; - fetchCalls = new Subject(); + fetchCalls = new BehaviorSubject( + this.buildParams(this.curPage, this.curPage.pageIndex), + ); constructor( private callsSvc: CallsService, @@ -244,6 +246,7 @@ export class CallsComponent { this.subscriptions.add( this.fetchCalls .pipe( + debounceTime(500), switchMap((params) => { console.log("gc switchmap"); return this.callsSvc.getCalls(params); -- 2.48.1 From 2777ec7bdc61199b62a4a037e325733bc1f579bf Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 4 Feb 2025 22:06:37 -0500 Subject: [PATCH 35/58] words --- pkg/incidents/incstore/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 47ad24b..9113690 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -55,7 +55,7 @@ type Store interface { // CallIn returns whether an incident is in an call CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) - // TGsIn returns the talkgroups referenced by an incident as a map, primary for rbac use. + // TGsIn returns the talkgroups referenced by an incident as a map, primarily for rbac use. TGsIn(ctx context.Context, inc uuid.UUID) (talkgroups.PresenceMap, error) } -- 2.48.1 From f5e969cea38c380d23d6321a93e286cf3b97111a Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 6 Feb 2025 20:50:16 -0500 Subject: [PATCH 36/58] Use the share ID --- pkg/rest/share.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rest/share.go b/pkg/rest/share.go index b0846da..07a161d 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -98,7 +98,7 @@ func (s *api) respondShareHandler(ie EntityFunc) ShareHandlerFunc { } sRes := shareResponse{ - ID: id, + ID: share.ID, Type: share.Type, SharedItem: res, } -- 2.48.1 From 3e3d39332c8e3bc97cc602fbf3bf818439fc29a0 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 6 Feb 2025 20:51:30 -0500 Subject: [PATCH 37/58] works --- .../incident/incident.component.html | 2 +- .../incidents/incident/incident.component.ts | 10 ++++--- .../stillbox/src/app/share/share.service.ts | 7 +---- .../src/app/talkgroups/talkgroups.service.ts | 26 ++++++++++--------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 0bd0390..cd132ea 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -7,7 +7,7 @@ >playlist_play - @if (!inShare) { + @if (share == null) { diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index a92f01c..a868c8a 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -9,7 +9,7 @@ import { FormControl, FormsModule, } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -41,6 +41,7 @@ import { FmtDatePipe } from '../incidents.component'; import { MatMenuModule } from '@angular/material/menu'; import { Share } from '../../shares'; import { ShareService } from '../../share/share.service'; +import { TalkgroupService } from '../../talkgroups/talkgroups.service'; export interface EditDialogData { incID: string; @@ -153,7 +154,6 @@ export class IncidentEditDialogComponent { styleUrl: './incident.component.scss', }) export class IncidentComponent { - inShare = false; incPrime = new Subject(); inc$!: Observable; @Input() share?: Share; @@ -176,15 +176,17 @@ export class IncidentComponent { constructor( private route: ActivatedRoute, - private shareSvc: ShareService, private incSvc: IncidentsService, private location: Location, + private tgSvc: TalkgroupService, ) {} saveIncName(ev: Event) {} ngOnInit() { - this.inShare = this.shareSvc.isInShare(); + if (this.share) { + this.tgSvc.setShare(this.share); + } let incOb: Observable; if (this.route.component === this.constructor) { // loaded by route diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index 9a1723d..d13d118 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -4,17 +4,12 @@ import { map, Observable, switchMap } from 'rxjs'; import { IncidentRecord } from '../incidents'; import { CallRecord } from '../calls'; import { Share, ShareType } from '../shares'; -import { Router } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class ShareService { - constructor(private http: HttpClient, private router: Router) {} - - isInShare(): boolean { - return this.router.url.startsWith('/s/'); - } + constructor(private http: HttpClient) {} getShare(id: string): Observable { return this.http.get(`/share/${id}`); diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 9eb25f8..4c14a19 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -27,15 +27,24 @@ export interface TalkgroupsPaginated { }) export class TalkgroupService { private readonly _getTalkgroup = new Map>(); - private tgs$!: Observable; + private tgs$: Observable; private tags$!: Observable; - private fetchAll = new BehaviorSubject<'fetch'>('fetch'); + private fetchAll = new BehaviorSubject(null); private subscriptions = new Subscription(); constructor(private http: HttpClient) { + this.tgs$ = this.fetchAll.pipe( + switchMap((share) => this.getTalkgroups(share)), + shareReplay(), + ); this.tags$ = this.fetchAll.pipe( switchMap(() => this.getAllTags()), shareReplay(), ); + this.fillTgMap(); + } + + setShare(share: Share) { + this.fetchAll.next(share); } ngOnDestroy() { @@ -46,8 +55,8 @@ export class TalkgroupService { return this.http.get('/api/talkgroup/tags'); } - getTalkgroups(share: Share | null): Observable { - return this.http.get('/api/talkgroup/'); + getTalkgroups(share: Share|null): Observable { + return this.http.get(share ? `/share/${share.id}/talkgroups` : '/api/talkgroup/'); } getTalkgroup( @@ -55,9 +64,6 @@ export class TalkgroupService { tg: number, share: Share | null = null, ): Observable { - if (this._getTalkgroup.size < 1) { - this.fillTgMap(share); - } const key = this.tgKey(sys, tg); if (!this._getTalkgroup.get(key)) { let rs = new ReplaySubject(); @@ -101,11 +107,7 @@ export class TalkgroupService { return this.http.post('/api/talkgroup/', pagination); } - fillTgMap(share: Share | null) { - this.tgs$ = this.fetchAll.pipe( - switchMap(() => this.getTalkgroups(share)), - shareReplay(), - ); + fillTgMap() { this.subscriptions.add( this.tgs$.subscribe((tgs) => { tgs.forEach((tg) => { -- 2.48.1 From 57f6e1254b3b7490d600de9251c99a4c459a88a7 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 6 Feb 2025 21:06:06 -0500 Subject: [PATCH 38/58] broke authenticated tgs --- client/stillbox/src/app/share/share.service.ts | 11 ++++++++++- .../src/app/talkgroups/talkgroups.service.ts | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index d13d118..1bebd1f 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -4,12 +4,21 @@ import { map, Observable, switchMap } from 'rxjs'; import { IncidentRecord } from '../incidents'; import { CallRecord } from '../calls'; import { Share, ShareType } from '../shares'; +import { ActivatedRoute, Router } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class ShareService { - constructor(private http: HttpClient) {} + constructor(private http: HttpClient, private router: Router, private route: ActivatedRoute) {} + + inShare(): string|null { + if(this.router.url.startsWith('/s/')) { + return this.route.snapshot.paramMap.get('id'); + } + + return null; + } getShare(id: string): Observable { return this.http.get(`/share/${id}`); diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 4c14a19..812edae 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -5,11 +5,13 @@ import { Observable, ReplaySubject, shareReplay, + Subject, Subscription, switchMap, } from 'rxjs'; import { Talkgroup, TalkgroupUpdate, TGID } from '../talkgroup'; import { Share } from '../shares'; +import { ShareService } from '../share/share.service'; export interface Pagination { page: number; @@ -29,9 +31,9 @@ export class TalkgroupService { private readonly _getTalkgroup = new Map>(); private tgs$: Observable; private tags$!: Observable; - private fetchAll = new BehaviorSubject(null); + private fetchAll = new Subject(); private subscriptions = new Subscription(); - constructor(private http: HttpClient) { + constructor(private http: HttpClient, private shareSvc: ShareService) { this.tgs$ = this.fetchAll.pipe( switchMap((share) => this.getTalkgroups(share)), shareReplay(), @@ -40,10 +42,17 @@ export class TalkgroupService { switchMap(() => this.getAllTags()), shareReplay(), ); + let sh = this.shareSvc.inShare(); + console.log(sh); + if (sh) { + this.shareSvc.getShare(sh).subscribe(this.fetchAll); + } else { + this.fetchAll.next(null); + } this.fillTgMap(); } - setShare(share: Share) { + setShare(share: Share|null) { this.fetchAll.next(share); } -- 2.48.1 From b41b5418d11f9835311da6a8b39614d9bb876a82 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 7 Feb 2025 08:22:51 -0500 Subject: [PATCH 39/58] only when logged in --- .../stillbox/src/app/calls/calls.component.ts | 5 ---- .../stillbox/src/app/calls/calls.service.ts | 1 - .../incident/incident.component.html | 26 +++++++++++-------- .../stillbox/src/app/share/share.service.ts | 10 ++++--- .../src/app/shares/shares.component.spec.ts | 5 ++-- .../src/app/shares/shares.component.ts | 6 ++--- .../talkgroup-record.component.ts | 1 - .../src/app/talkgroups/talkgroups.service.ts | 22 +++++++++++----- 8 files changed, 41 insertions(+), 35 deletions(-) diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index dceda59..76b50df 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -180,7 +180,6 @@ export class CallsComponent { } setPage(p: PageEvent, force?: boolean) { - console.log("setpage") this.selection.clear(); this.curPage = p; if (p && p!.pageSize != this.perPage) { @@ -196,19 +195,16 @@ export class CallsComponent { } getCalls(p: PageEvent, force?: boolean) { - console.log("getcalls") const pageStart = p.pageIndex * p.pageSize; const serverPage = Math.floor(pageStart / reqPageSize) + 1; this.pageWindow = pageStart % reqPageSize; if (serverPage == this.currentServerPage && !force && this.currentSet) { - console.log("currentset"); this.callsResult.next( this.callsResult ? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize) : [], ); } else { - console.log("not currentset"); this.currentServerPage = serverPage; this.fetchCalls.next(this.buildParams(p, serverPage)); } @@ -248,7 +244,6 @@ export class CallsComponent { .pipe( debounceTime(500), switchMap((params) => { - console.log("gc switchmap"); return this.callsSvc.getCalls(params); }), ) diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index 1d6c8ee..1b433e8 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -131,7 +131,6 @@ export class SafePipe implements PipeTransform { return this._sanitizer.bypassSecurityTrustUrl(value); case 'resourceUrl': let res = this._sanitizer.bypassSecurityTrustResourceUrl(value); - console.log(res); return res; default: return this._sanitizer.bypassSecurityTrustHtml(value); diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index cd132ea..68e8c6e 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -7,18 +7,22 @@ >playlist_play - @if (share == null) { - - - - - + + + + }
diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index 1bebd1f..75d8a1f 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -10,10 +10,14 @@ import { ActivatedRoute, Router } from '@angular/router'; providedIn: 'root', }) export class ShareService { - constructor(private http: HttpClient, private router: Router, private route: ActivatedRoute) {} + constructor( + private http: HttpClient, + private router: Router, + private route: ActivatedRoute, + ) {} - inShare(): string|null { - if(this.router.url.startsWith('/s/')) { + inShare(): string | null { + if (this.router.url.startsWith('/s/')) { return this.route.snapshot.paramMap.get('id'); } diff --git a/client/stillbox/src/app/shares/shares.component.spec.ts b/client/stillbox/src/app/shares/shares.component.spec.ts index a995779..107f8aa 100644 --- a/client/stillbox/src/app/shares/shares.component.spec.ts +++ b/client/stillbox/src/app/shares/shares.component.spec.ts @@ -8,9 +8,8 @@ describe('SharesComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SharesComponent] - }) - .compileComponents(); + imports: [SharesComponent], + }).compileComponents(); fixture = TestBed.createComponent(SharesComponent); component = fixture.componentInstance; diff --git a/client/stillbox/src/app/shares/shares.component.ts b/client/stillbox/src/app/shares/shares.component.ts index dcd17d0..8bbcdea 100644 --- a/client/stillbox/src/app/shares/shares.component.ts +++ b/client/stillbox/src/app/shares/shares.component.ts @@ -4,8 +4,6 @@ import { Component } from '@angular/core'; selector: 'app-shares', imports: [], templateUrl: './shares.component.html', - styleUrl: './shares.component.scss' + styleUrl: './shares.component.scss', }) -export class SharesComponent { - -} +export class SharesComponent {} diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index ae0937c..75efd33 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -158,7 +158,6 @@ export class TalkgroupRecordComponent { .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) .pipe( tap((tg) => { - console.log('tap run'); tg.alert_rules = tg.alert_rules ? tg.alert_rules.map((x) => Object.assign(new AlertRule(), x)) : []; diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 812edae..7fc6026 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -12,6 +12,7 @@ import { import { Talkgroup, TalkgroupUpdate, TGID } from '../talkgroup'; import { Share } from '../shares'; import { ShareService } from '../share/share.service'; +import { AuthService } from '../login/auth.service'; export interface Pagination { page: number; @@ -31,9 +32,13 @@ export class TalkgroupService { private readonly _getTalkgroup = new Map>(); private tgs$: Observable; private tags$!: Observable; - private fetchAll = new Subject(); + private fetchAll = new ReplaySubject(); private subscriptions = new Subscription(); - constructor(private http: HttpClient, private shareSvc: ShareService) { + constructor( + private http: HttpClient, + private shareSvc: ShareService, + private authSvc: AuthService, + ) { this.tgs$ = this.fetchAll.pipe( switchMap((share) => this.getTalkgroups(share)), shareReplay(), @@ -43,16 +48,17 @@ export class TalkgroupService { shareReplay(), ); let sh = this.shareSvc.inShare(); - console.log(sh); if (sh) { this.shareSvc.getShare(sh).subscribe(this.fetchAll); } else { - this.fetchAll.next(null); + if (this.authSvc.loggedIn) { + this.fetchAll.next(null); + } } this.fillTgMap(); } - setShare(share: Share|null) { + setShare(share: Share | null) { this.fetchAll.next(share); } @@ -64,8 +70,10 @@ export class TalkgroupService { return this.http.get('/api/talkgroup/tags'); } - getTalkgroups(share: Share|null): Observable { - return this.http.get(share ? `/share/${share.id}/talkgroups` : '/api/talkgroup/'); + getTalkgroups(share: Share | null): Observable { + return this.http.get( + share ? `/share/${share.id}/talkgroups` : '/api/talkgroup/', + ); } getTalkgroup( -- 2.48.1 From 1b2df3c6a35d78cb8bba9e9107dc9bf120255532 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 7 Feb 2025 08:49:01 -0500 Subject: [PATCH 40/58] Fix m3u --- .../src/app/incidents/incident/incident.component.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 68e8c6e..8a8b159 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -3,7 +3,12 @@

{{ inc?.name }} - playlist_play

-- 2.48.1 From d0d3f503b85f4038cea995fa868067d363e7deec Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 7 Feb 2025 09:24:42 -0500 Subject: [PATCH 41/58] camel case --- pkg/calls/call.go | 8 +- pkg/database/batch.go | 10 +-- pkg/database/calls.sql.go | 64 +++++++-------- pkg/database/incidents.sql.go | 28 +++---- pkg/database/models.go | 82 +++++++++---------- pkg/database/share.sql.go | 6 +- pkg/database/talkgroups.sql.go | 24 +++--- pkg/database/users.sql.go | 6 +- .../xport/radioref/testdata/riscon.json | 2 +- sql/sqlc.yaml | 1 + 10 files changed, 116 insertions(+), 115 deletions(-) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index da400c3..1639327 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -68,18 +68,18 @@ type Call struct { AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"` AudioURL *string `json:"audioURL,omitempty" relayOut:"audioURL,omitempty"` Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"` - DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"` + DateTime time.Time `json:"callDate,omitempty" relayOut:"dateTime,omitempty"` Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"` Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"` Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"` Source int `json:"source,omitempty" relayOut:"source,omitempty"` - System int `json:"system_id,omitempty" relayOut:"system,omitempty"` + System int `json:"systemId,omitempty" relayOut:"system,omitempty"` Submitter *users.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"` - SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"` + SystemLabel string `json:"systemName,omitempty" relayOut:"systemLabel,omitempty"` Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"` TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"` TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"` - TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"` + TGAlphaTag *string `json:"tgAlphaTag,omitempty" relayOut:"talkgroupTag,omitempty"` Transcript *string `json:"transcript" relayOut:"transcript,omitempty"` shouldStore bool `json:"-"` diff --git a/pkg/database/batch.go b/pkg/database/batch.go index 3001e0d..e7cf721 100644 --- a/pkg/database/batch.go +++ b/pkg/database/batch.go @@ -57,7 +57,7 @@ type StoreTGVersionBatchResults struct { type StoreTGVersionParams struct { Submitter *int32 `json:"submitter"` - SystemID int32 `json:"system_id"` + SystemID int32 `json:"systemId"` TGID int32 `json:"tgid"` } @@ -135,16 +135,16 @@ type UpsertTalkgroupBatchResults struct { } type UpsertTalkgroupParams struct { - SystemID int32 `json:"system_id"` + SystemID int32 `json:"systemId"` TGID int32 `json:"tgid"` Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TGGroup *string `json:"tg_group"` + AlphaTag *string `json:"alphaTag"` + TGGroup *string `json:"tgGroup"` Frequency *int32 `json:"frequency"` Metadata jsontypes.Metadata `json:"metadata"` Tags []string `json:"tags"` Alert interface{} `json:"alert"` - AlertRules rules.AlertRules `json:"alert_rules"` + AlertRules rules.AlertRules `json:"alertRules"` Weight pgtype.Numeric `json:"weight"` Learned *bool `json:"learned"` } diff --git a/pkg/database/calls.sql.go b/pkg/database/calls.sql.go index f614d0d..0d8c2d0 100644 --- a/pkg/database/calls.sql.go +++ b/pkg/database/calls.sql.go @@ -30,10 +30,10 @@ VALUES type AddAlertParams struct { Time pgtype.Timestamptz `json:"time"` TGID int `json:"tgid"` - SystemID int `json:"system_id"` + SystemID int `json:"systemId"` Weight *float32 `json:"weight"` Score *float32 `json:"score"` - OrigScore *float32 `json:"orig_score"` + OrigScore *float32 `json:"origScore"` Notified bool `json:"notified"` Metadata []byte `json:"metadata"` } @@ -97,18 +97,18 @@ type AddCallParams struct { Submitter *int32 `json:"submitter"` System int `json:"system"` Talkgroup int `json:"talkgroup"` - CallDate pgtype.Timestamptz `json:"call_date"` - AudioName *string `json:"audio_name"` - AudioBlob []byte `json:"audio_blob"` - AudioType *string `json:"audio_type"` - AudioUrl *string `json:"audio_url"` + CallDate pgtype.Timestamptz `json:"callDate"` + AudioName *string `json:"audioName"` + AudioBlob []byte `json:"audioBlob"` + AudioType *string `json:"audioType"` + AudioUrl *string `json:"audioUrl"` Duration *int32 `json:"duration"` Frequency int `json:"frequency"` Frequencies []int `json:"frequencies"` Patches []int `json:"patches"` - TGLabel *string `json:"tg_label"` - TGAlphaTag *string `json:"tg_alpha_tag"` - TGGroup *string `json:"tg_group"` + TGLabel *string `json:"tgLabel"` + TGAlphaTag *string `json:"tgAlphaTag"` + TGGroup *string `json:"tgGroup"` Source int `json:"source"` } @@ -192,17 +192,17 @@ type GetCallRow struct { Submitter *int32 `json:"submitter"` System int `json:"system"` Talkgroup int `json:"talkgroup"` - CallDate pgtype.Timestamptz `json:"call_date"` - AudioName *string `json:"audio_name"` - AudioType *string `json:"audio_type"` - AudioUrl *string `json:"audio_url"` + CallDate pgtype.Timestamptz `json:"callDate"` + AudioName *string `json:"audioName"` + AudioType *string `json:"audioType"` + AudioUrl *string `json:"audioUrl"` Duration *int32 `json:"duration"` Frequency int `json:"frequency"` Frequencies []int `json:"frequencies"` Patches []int `json:"patches"` - TGLabel *string `json:"tg_label"` - TGAlphaTag *string `json:"tg_alpha_tag"` - TGGroup *string `json:"tg_group"` + TGLabel *string `json:"tgLabel"` + TGAlphaTag *string `json:"tgAlphaTag"` + TGGroup *string `json:"tgGroup"` Source int `json:"source"` Transcript *string `json:"transcript"` } @@ -251,10 +251,10 @@ WHERE sc.id = $1 ` type GetCallAudioByIDRow struct { - CallDate pgtype.Timestamptz `json:"call_date"` - AudioName *string `json:"audio_name"` - AudioType *string `json:"audio_type"` - AudioBlob []byte `json:"audio_blob"` + CallDate pgtype.Timestamptz `json:"callDate"` + AudioName *string `json:"audioName"` + AudioType *string `json:"audioType"` + AudioBlob []byte `json:"audioBlob"` } func (q *Queries) GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error) { @@ -318,10 +318,10 @@ CASE WHEN $4::TEXT[] IS NOT NULL THEN type ListCallsCountParams struct { Start pgtype.Timestamptz `json:"start"` End pgtype.Timestamptz `json:"end"` - TagsAny []string `json:"tags_any"` - TagsNot []string `json:"tags_not"` - TGFilter *string `json:"tg_filter"` - LongerThan pgtype.Numeric `json:"longer_than"` + TagsAny []string `json:"tagsAny"` + TagsNot []string `json:"tagsNot"` + TGFilter *string `json:"tgFilter"` + LongerThan pgtype.Numeric `json:"longerThan"` } func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error) { @@ -378,20 +378,20 @@ FETCH NEXT $9 ROWS ONLY type ListCallsPParams struct { Start pgtype.Timestamptz `json:"start"` End pgtype.Timestamptz `json:"end"` - TagsAny []string `json:"tags_any"` - TagsNot []string `json:"tags_not"` - TGFilter *string `json:"tg_filter"` - LongerThan pgtype.Numeric `json:"longer_than"` + TagsAny []string `json:"tagsAny"` + TagsNot []string `json:"tagsNot"` + TGFilter *string `json:"tgFilter"` + LongerThan pgtype.Numeric `json:"longerThan"` Direction string `json:"direction"` Offset int32 `json:"offset"` - PerPage int32 `json:"per_page"` + PerPage int32 `json:"perPage"` } type ListCallsPRow struct { ID uuid.UUID `json:"id"` - CallDate pgtype.Timestamptz `json:"call_date"` + CallDate pgtype.Timestamptz `json:"callDate"` Duration *int32 `json:"duration"` - SystemID int `json:"system_id"` + SystemID int `json:"systemId"` TGID int `json:"tgid"` Incidents int64 `json:"incidents"` } diff --git a/pkg/database/incidents.sql.go b/pkg/database/incidents.sql.go index b233955..a894f1e 100644 --- a/pkg/database/incidents.sql.go +++ b/pkg/database/incidents.sql.go @@ -83,8 +83,8 @@ type CreateIncidentParams struct { Name string `json:"name"` Owner int `json:"owner"` Description *string `json:"description"` - StartTime pgtype.Timestamptz `json:"start_time"` - EndTime pgtype.Timestamptz `json:"end_time"` + StartTime pgtype.Timestamptz `json:"startTime"` + EndTime pgtype.Timestamptz `json:"endTime"` Location []byte `json:"location"` Metadata jsontypes.Metadata `json:"metadata"` } @@ -206,16 +206,16 @@ ORDER BY ic.call_date ASC ` type GetIncidentCallsRow struct { - CallID uuid.UUID `json:"call_id"` - CallDate pgtype.Timestamptz `json:"call_date"` + CallID uuid.UUID `json:"callId"` + CallDate pgtype.Timestamptz `json:"callDate"` Duration *int32 `json:"duration"` - SystemID int `json:"system_id"` + SystemID int `json:"systemId"` TGID int `json:"tgid"` Notes []byte `json:"notes"` Submitter *int32 `json:"submitter"` - AudioName *string `json:"audio_name"` - AudioType *string `json:"audio_type"` - AudioUrl *string `json:"audio_url"` + AudioName *string `json:"audioName"` + AudioType *string `json:"audioType"` + AudioUrl *string `json:"audioUrl"` Frequency int `json:"frequency"` Frequencies []int `json:"frequencies"` Patches []int `json:"patches"` @@ -327,7 +327,7 @@ type ListIncidentsPParams struct { Filter *string `json:"filter"` Direction string `json:"direction"` Offset int32 `json:"offset"` - PerPage int32 `json:"per_page"` + PerPage int32 `json:"perPage"` } type ListIncidentsPRow struct { @@ -335,11 +335,11 @@ type ListIncidentsPRow struct { Name string `json:"name"` Owner int `json:"owner"` Description *string `json:"description"` - StartTime pgtype.Timestamptz `json:"start_time"` - EndTime pgtype.Timestamptz `json:"end_time"` + StartTime pgtype.Timestamptz `json:"startTime"` + EndTime pgtype.Timestamptz `json:"endTime"` Location []byte `json:"location"` Metadata jsontypes.Metadata `json:"metadata"` - CallsCount int64 `json:"calls_count"` + CallsCount int64 `json:"callsCount"` } func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error) { @@ -417,8 +417,8 @@ RETURNING id, name, owner, description, start_time, end_time, location, metadata type UpdateIncidentParams struct { Name *string `json:"name"` Description *string `json:"description"` - StartTime pgtype.Timestamptz `json:"start_time"` - EndTime pgtype.Timestamptz `json:"end_time"` + StartTime pgtype.Timestamptz `json:"startTime"` + EndTime pgtype.Timestamptz `json:"endTime"` Location []byte `json:"location"` Metadata jsontypes.Metadata `json:"metadata"` ID uuid.UUID `json:"id"` diff --git a/pkg/database/models.go b/pkg/database/models.go index 585f860..2f470b6 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -17,10 +17,10 @@ type Alert struct { ID int `json:"id,omitempty"` Time pgtype.Timestamptz `json:"time,omitempty"` TGID int `json:"tgid,omitempty"` - SystemID int `json:"system_id,omitempty"` + SystemID int `json:"systemId,omitempty"` Weight *float32 `json:"weight,omitempty"` Score *float32 `json:"score,omitempty"` - OrigScore *float32 `json:"orig_score,omitempty"` + OrigScore *float32 `json:"origScore,omitempty"` Notified bool `json:"notified,omitempty"` Metadata []byte `json:"metadata,omitempty"` } @@ -28,10 +28,10 @@ type Alert struct { type ApiKey struct { ID int `json:"id,omitempty"` Owner int `json:"owner,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` Expires pgtype.Timestamp `json:"expires,omitempty"` Disabled *bool `json:"disabled,omitempty"` - ApiKey string `json:"api_key,omitempty"` + ApiKey string `json:"apiKey,omitempty"` } type Call struct { @@ -39,18 +39,18 @@ type Call struct { Submitter *int32 `json:"submitter,omitempty"` System int `json:"system,omitempty"` Talkgroup int `json:"talkgroup,omitempty"` - CallDate pgtype.Timestamptz `json:"call_date,omitempty"` - AudioName *string `json:"audio_name,omitempty"` - AudioBlob []byte `json:"audio_blob,omitempty"` + CallDate pgtype.Timestamptz `json:"callDate,omitempty"` + AudioName *string `json:"audioName,omitempty"` + AudioBlob []byte `json:"audioBlob,omitempty"` Duration *int32 `json:"duration,omitempty"` - AudioType *string `json:"audio_type,omitempty"` - AudioUrl *string `json:"audio_url,omitempty"` + AudioType *string `json:"audioType,omitempty"` + AudioUrl *string `json:"audioUrl,omitempty"` Frequency int `json:"frequency,omitempty"` Frequencies []int `json:"frequencies,omitempty"` Patches []int `json:"patches,omitempty"` - TGLabel *string `json:"tg_label,omitempty"` - TGAlphaTag *string `json:"tg_alpha_tag,omitempty"` - TGGroup *string `json:"tg_group,omitempty"` + TGLabel *string `json:"tgLabel,omitempty"` + TGAlphaTag *string `json:"tgAlphaTag,omitempty"` + TGGroup *string `json:"tgGroup,omitempty"` Source int `json:"source,omitempty"` Transcript *string `json:"transcript,omitempty"` } @@ -60,32 +60,32 @@ type Incident struct { Name string `json:"name,omitempty"` Owner int `json:"owner,omitempty"` Description *string `json:"description,omitempty"` - StartTime pgtype.Timestamptz `json:"start_time,omitempty"` - EndTime pgtype.Timestamptz `json:"end_time,omitempty"` + StartTime pgtype.Timestamptz `json:"startTime,omitempty"` + EndTime pgtype.Timestamptz `json:"endTime,omitempty"` Location []byte `json:"location,omitempty"` Metadata jsontypes.Metadata `json:"metadata,omitempty"` } type IncidentsCall struct { - IncidentID uuid.UUID `json:"incident_id,omitempty"` - CallID uuid.UUID `json:"call_id,omitempty"` - CallsTblID pgtype.UUID `json:"calls_tbl_id,omitempty"` - SweptCallID pgtype.UUID `json:"swept_call_id,omitempty"` - CallDate pgtype.Timestamptz `json:"call_date,omitempty"` + IncidentID uuid.UUID `json:"incidentId,omitempty"` + CallID uuid.UUID `json:"callId,omitempty"` + CallsTblID pgtype.UUID `json:"callsTblId,omitempty"` + SweptCallID pgtype.UUID `json:"sweptCallId,omitempty"` + CallDate pgtype.Timestamptz `json:"callDate,omitempty"` Notes []byte `json:"notes,omitempty"` } type Setting struct { Name string `json:"name,omitempty"` - UpdatedBy *int32 `json:"updated_by,omitempty"` + UpdatedBy *int32 `json:"updatedBy,omitempty"` Value []byte `json:"value,omitempty"` } type Share struct { ID string `json:"id,omitempty"` - EntityType string `json:"entity_type,omitempty"` - EntityID uuid.UUID `json:"entity_id,omitempty"` - EntityDate pgtype.Timestamptz `json:"entity_date,omitempty"` + EntityType string `json:"entityType,omitempty"` + EntityID uuid.UUID `json:"entityId,omitempty"` + EntityDate pgtype.Timestamptz `json:"entityDate,omitempty"` Owner int `json:"owner,omitempty"` Expiration pgtype.Timestamptz `json:"expiration,omitempty"` } @@ -95,18 +95,18 @@ type SweptCall struct { Submitter *int32 `json:"submitter,omitempty"` System int `json:"system,omitempty"` Talkgroup int `json:"talkgroup,omitempty"` - CallDate pgtype.Timestamptz `json:"call_date,omitempty"` - AudioName *string `json:"audio_name,omitempty"` - AudioBlob []byte `json:"audio_blob,omitempty"` + CallDate pgtype.Timestamptz `json:"callDate,omitempty"` + AudioName *string `json:"audioName,omitempty"` + AudioBlob []byte `json:"audioBlob,omitempty"` Duration *int32 `json:"duration,omitempty"` - AudioType *string `json:"audio_type,omitempty"` - AudioUrl *string `json:"audio_url,omitempty"` + AudioType *string `json:"audioType,omitempty"` + AudioUrl *string `json:"audioUrl,omitempty"` Frequency int `json:"frequency,omitempty"` Frequencies []int `json:"frequencies,omitempty"` Patches []int `json:"patches,omitempty"` - TGLabel *string `json:"tg_label,omitempty"` - TGAlphaTag *string `json:"tg_alpha_tag,omitempty"` - TGGroup *string `json:"tg_group,omitempty"` + TGLabel *string `json:"tgLabel,omitempty"` + TGAlphaTag *string `json:"tgAlphaTag,omitempty"` + TGGroup *string `json:"tgGroup,omitempty"` Source int `json:"source,omitempty"` Transcript *string `json:"transcript,omitempty"` } @@ -118,16 +118,16 @@ type System struct { type Talkgroup struct { ID int `json:"id,omitempty"` - SystemID int32 `json:"system_id,omitempty"` + SystemID int32 `json:"systemId,omitempty"` TGID int32 `json:"tgid,omitempty"` Name *string `json:"name,omitempty"` - AlphaTag *string `json:"alpha_tag,omitempty"` - TGGroup *string `json:"tg_group,omitempty"` + AlphaTag *string `json:"alphaTag,omitempty"` + TGGroup *string `json:"tgGroup,omitempty"` Frequency *int32 `json:"frequency,omitempty"` Metadata jsontypes.Metadata `json:"metadata,omitempty"` Tags []string `json:"tags,omitempty"` Alert bool `json:"alert,omitempty"` - AlertRules rules.AlertRules `json:"alert_rules,omitempty"` + AlertRules rules.AlertRules `json:"alertRules,omitempty"` Weight float32 `json:"weight,omitempty"` Learned bool `json:"learned,omitempty"` Ignored bool `json:"ignored,omitempty"` @@ -136,18 +136,18 @@ type Talkgroup struct { type TalkgroupVersion struct { ID int `json:"id,omitempty"` Time pgtype.Timestamptz `json:"time,omitempty"` - CreatedBy *int32 `json:"created_by,omitempty"` + CreatedBy *int32 `json:"createdBy,omitempty"` Deleted *bool `json:"deleted,omitempty"` - SystemID *int32 `json:"system_id,omitempty"` + SystemID *int32 `json:"systemId,omitempty"` TGID *int32 `json:"tgid,omitempty"` Name *string `json:"name,omitempty"` - AlphaTag *string `json:"alpha_tag,omitempty"` - TGGroup *string `json:"tg_group,omitempty"` + AlphaTag *string `json:"alphaTag,omitempty"` + TGGroup *string `json:"tgGroup,omitempty"` Frequency *int32 `json:"frequency,omitempty"` Metadata []byte `json:"metadata,omitempty"` Tags []string `json:"tags,omitempty"` Alert *bool `json:"alert,omitempty"` - AlertRules []byte `json:"alert_rules,omitempty"` + AlertRules []byte `json:"alertRules,omitempty"` Weight *float32 `json:"weight,omitempty"` Learned *bool `json:"learned,omitempty"` Ignored *bool `json:"ignored,omitempty"` @@ -158,6 +158,6 @@ type User struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Email string `json:"email,omitempty"` - IsAdmin bool `json:"is_admin,omitempty"` + IsAdmin bool `json:"isAdmin,omitempty"` Prefs []byte `json:"prefs,omitempty"` } diff --git a/pkg/database/share.sql.go b/pkg/database/share.sql.go index fbd2829..ac7bc38 100644 --- a/pkg/database/share.sql.go +++ b/pkg/database/share.sql.go @@ -25,9 +25,9 @@ INSERT INTO shares ( type CreateShareParams struct { ID string `json:"id"` - EntityType string `json:"entity_type"` - EntityID uuid.UUID `json:"entity_id"` - EntityDate pgtype.Timestamptz `json:"entity_date"` + EntityType string `json:"entityType"` + EntityID uuid.UUID `json:"entityId"` + EntityDate pgtype.Timestamptz `json:"entityDate"` Owner int `json:"owner"` Expiration pgtype.Timestamptz `json:"expiration"` } diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go index 8023101..2ad8bae 100644 --- a/pkg/database/talkgroups.sql.go +++ b/pkg/database/talkgroups.sql.go @@ -32,11 +32,11 @@ INSERT INTO talkgroups( ` type AddLearnedTalkgroupParams struct { - SystemID int32 `json:"system_id"` + SystemID int32 `json:"systemId"` TGID int32 `json:"tgid"` Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TGGroup *string `json:"tg_group"` + AlphaTag *string `json:"alphaTag"` + TGGroup *string `json:"tgGroup"` } func (q *Queries) AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error) { @@ -202,7 +202,7 @@ AND NOT (tags @> ARRAY[$3]) ` type GetTalkgroupIDsByTagsRow struct { - SystemID int32 `json:"system_id"` + SystemID int32 `json:"systemId"` TGID int32 `json:"tgid"` } @@ -511,9 +511,9 @@ FETCH NEXT $5 ROWS ONLY type GetTalkgroupsWithLearnedBySystemPParams struct { System int32 `json:"system"` Filter *string `json:"filter"` - OrderBy string `json:"order_by"` + OrderBy string `json:"orderBy"` Offset int32 `json:"offset"` - PerPage int32 `json:"per_page"` + PerPage int32 `json:"perPage"` } type GetTalkgroupsWithLearnedBySystemPRow struct { @@ -611,9 +611,9 @@ FETCH NEXT $4 ROWS ONLY type GetTalkgroupsWithLearnedPParams struct { Filter *string `json:"filter"` - OrderBy string `json:"order_by"` + OrderBy string `json:"orderBy"` Offset int32 `json:"offset"` - PerPage int32 `json:"per_page"` + PerPage int32 `json:"perPage"` } type GetTalkgroupsWithLearnedPRow struct { @@ -774,17 +774,17 @@ RETURNING id, system_id, tgid, name, alpha_tag, tg_group, frequency, metadata, t type UpdateTalkgroupParams struct { Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TGGroup *string `json:"tg_group"` + AlphaTag *string `json:"alphaTag"` + TGGroup *string `json:"tgGroup"` Frequency *int32 `json:"frequency"` Metadata jsontypes.Metadata `json:"metadata"` Tags []string `json:"tags"` Alert *bool `json:"alert"` - AlertRules rules.AlertRules `json:"alert_rules"` + AlertRules rules.AlertRules `json:"alertRules"` Weight *float32 `json:"weight"` Learned *bool `json:"learned"` ID *int32 `json:"id"` - SystemID *int32 `json:"system_id"` + SystemID *int32 `json:"systemId"` TGID *int32 `json:"tgid"` } diff --git a/pkg/database/users.sql.go b/pkg/database/users.sql.go index 74f2b43..2f38048 100644 --- a/pkg/database/users.sql.go +++ b/pkg/database/users.sql.go @@ -51,7 +51,7 @@ type CreateUserParams struct { Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` - IsAdmin bool `json:"is_admin"` + IsAdmin bool `json:"isAdmin"` } func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { @@ -108,10 +108,10 @@ WHERE api_key = $1 type GetAPIKeyRow struct { ID int `json:"id"` Owner int `json:"owner"` - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"createdAt"` Expires pgtype.Timestamp `json:"expires"` Disabled *bool `json:"disabled"` - ApiKey string `json:"api_key"` + ApiKey string `json:"apiKey"` Username string `json:"username"` } diff --git a/pkg/talkgroups/xport/radioref/testdata/riscon.json b/pkg/talkgroups/xport/radioref/testdata/riscon.json index 596c99e..aa8c64e 100644 --- a/pkg/talkgroups/xport/radioref/testdata/riscon.json +++ b/pkg/talkgroups/xport/radioref/testdata/riscon.json @@ -1 +1 @@ -[{"id":0,"system_id":197,"tgid":2,"name":"Intercity Fire","alpha_tag":"Intercity FD","tg_group":"Statewide Mutual Aid/Intersystem","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":1,"system_id":197,"tgid":3,"name":"Intercity Police","alpha_tag":"Intercity PD","tg_group":"Statewide Mutual Aid/Intersystem","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":2,"system_id":197,"tgid":21,"name":"North Dispatch ","alpha_tag":"RISP N Disp","tg_group":"State Police - District A (North)","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":3,"system_id":197,"tgid":22,"name":"North Car-to-Car/Information","alpha_tag":"RISP N Car","tg_group":"State Police - District A (North)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":4,"system_id":197,"tgid":24,"name":"North Tactical Ops 1","alpha_tag":"RISP N Tac 1","tg_group":"State Police - District A (North)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":5,"system_id":197,"tgid":23,"name":"North Tactical Ops 2","alpha_tag":"RISP N Tac 2","tg_group":"State Police - District A (North)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":6,"system_id":197,"tgid":25,"name":"South Dispatch ","alpha_tag":"RISP S Disp","tg_group":"State Police - District B (South)","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":7,"system_id":197,"tgid":27,"name":"South Car-to-Car/Information","alpha_tag":"RISP S Car","tg_group":"State Police - District B (South)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":8,"system_id":197,"tgid":16,"name":"State Fire Marshall","alpha_tag":"State FMO","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":9,"system_id":197,"tgid":1038,"name":"Northern Rhode Island Fire Chiefs","alpha_tag":"NRI Fire Chi","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":10,"system_id":197,"tgid":1041,"name":"Southern Rhode Island Fire Chiefs","alpha_tag":"SRI Fire Chi","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":11,"system_id":197,"tgid":1314,"name":"Tanker Taskforce 1","alpha_tag":"Tanker TF 1","tg_group":"Statewide Fire","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":12,"system_id":197,"tgid":194,"name":"Lifepact Ambulance (Statewide)","alpha_tag":"Lifepact Amb","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":13,"system_id":197,"tgid":212,"name":"Fatima St Josephs","alpha_tag":"Fatima-St Joes","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["Business"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":14,"system_id":197,"tgid":220,"name":"Health 1","alpha_tag":"Health 1","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":{"encrypted":true},"tags":["EMS-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":15,"system_id":197,"tgid":221,"name":"Health 2","alpha_tag":"Health 2","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":{"encrypted":true},"tags":["EMS-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":16,"system_id":197,"tgid":222,"name":"Department of Health - Statewide","alpha_tag":"Dept of HealthSW","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["EMS-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":17,"system_id":197,"tgid":228,"name":"DMAT South","alpha_tag":"DMAT South","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":{"encrypted":true},"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":18,"system_id":197,"tgid":232,"name":"Life Span Net 1","alpha_tag":"Life Span 1","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["EMS-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":19,"system_id":197,"tgid":234,"name":"RI Hospital Operations","alpha_tag":"RI Hosp Ops","tg_group":"Statewide EMS and Hospitals","frequency":null,"metadata":null,"tags":["Business"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":20,"system_id":197,"tgid":120,"name":"Law Enforcement Operations","alpha_tag":"DEM PD Ops","tg_group":"Department of Environmental Management","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":21,"system_id":197,"tgid":122,"name":"Law Enforcement Police","alpha_tag":"DEM Police","tg_group":"Department of Environmental Management","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":22,"system_id":197,"tgid":10,"name":"Emergency Management Agency 1","alpha_tag":"EMA-1","tg_group":"Emergency Management Agency","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":23,"system_id":197,"tgid":20,"name":"Emergency Management Agency","alpha_tag":"EMA","tg_group":"Emergency Management Agency","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":24,"system_id":197,"tgid":4,"name":"Wide Area 3","alpha_tag":"Wide Area 3","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":25,"system_id":197,"tgid":5,"name":"Wide Area 4","alpha_tag":"Wide Area 4","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":26,"system_id":197,"tgid":6,"name":"Wide Area 5","alpha_tag":"Wide Area 5","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":27,"system_id":197,"tgid":7,"name":"Wide Area 6","alpha_tag":"Wide Area 6","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":28,"system_id":197,"tgid":1018,"name":"Southwide CH-1","alpha_tag":"SOUTHWIDE 1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":29,"system_id":197,"tgid":1019,"name":"Southwide CH-2","alpha_tag":"SOUTHWIDE 2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":30,"system_id":197,"tgid":1022,"name":"Wide Area 7","alpha_tag":"WIDE AREA 7","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":31,"system_id":197,"tgid":1023,"name":"Wide Area 8","alpha_tag":"WIDE AREA 8","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":32,"system_id":197,"tgid":1025,"name":"Inland Marine Interop","alpha_tag":"Inland Marine IO","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":33,"system_id":197,"tgid":1037,"name":"Southside CH 5","alpha_tag":"SOUTHSIDE 5","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":34,"system_id":197,"tgid":1173,"name":"North Wide 1","alpha_tag":"NORTHWIDE1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":35,"system_id":197,"tgid":1174,"name":"North Wide 2","alpha_tag":"NORTHWIDE2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":36,"system_id":197,"tgid":1177,"name":"North Wide 5","alpha_tag":"NORTHWIDE5","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":37,"system_id":197,"tgid":1185,"name":"Metro Wide 1","alpha_tag":"METROWIDE1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":38,"system_id":197,"tgid":1186,"name":"Metro Wide 2","alpha_tag":"METROWIDE2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":39,"system_id":197,"tgid":1187,"name":"Metro Wide 3","alpha_tag":"METROWIDE3","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":40,"system_id":197,"tgid":1335,"name":"East Wide 1","alpha_tag":"EASTWIDE 1","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":41,"system_id":197,"tgid":1336,"name":"East Wide 2","alpha_tag":"EASTWIDE 2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":42,"system_id":197,"tgid":1337,"name":"East Wide 3","alpha_tag":"EASTWIDE 3","tg_group":"Statewide Area/Events","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":43,"system_id":197,"tgid":11186,"name":"Metro Wide 2","alpha_tag":"METROWIDE2","tg_group":"Statewide Area/Events","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":44,"system_id":197,"tgid":1033,"name":"Tanker Taskforce ","alpha_tag":"TANK TF","tg_group":"Statewide Emergency Response","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":45,"system_id":197,"tgid":1034,"name":"Hazmat 1","alpha_tag":"HZT DC1","tg_group":"Statewide Emergency Response","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":46,"system_id":197,"tgid":1035,"name":"Hazmat 2","alpha_tag":"HZT DC2","tg_group":"Statewide Emergency Response","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":47,"system_id":197,"tgid":176,"name":"Department of Transportation - Primary","alpha_tag":"RIDOT Primary","tg_group":"Department of Transportation","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":48,"system_id":197,"tgid":4421,"name":"Newport Pell Bridge Operations","alpha_tag":"RITBA - Pell Bdg","tg_group":"Tunnel and Bridge Authority","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":49,"system_id":197,"tgid":274,"name":"Providence VA Police","alpha_tag":"VA Police","tg_group":"Federal","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":50,"system_id":197,"tgid":186,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":{"encrypted":true},"tags":["Transportation"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":51,"system_id":197,"tgid":187,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":52,"system_id":197,"tgid":188,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":53,"system_id":197,"tgid":189,"name":"Rhode Island Public Transit Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":54,"system_id":197,"tgid":190,"name":"Rhode Island Public Transit. Auth.","alpha_tag":"RIPTA","tg_group":"RIPTA","frequency":null,"metadata":null,"tags":["Transportation"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":55,"system_id":197,"tgid":304,"name":"Fire Operations","alpha_tag":"Quonset ANGB FD","tg_group":"Quonset ANGB","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":56,"system_id":197,"tgid":17,"name":"Airport Police Operations","alpha_tag":"TF Green PD","tg_group":"Rhode Island Airport Commission","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":57,"system_id":197,"tgid":19,"name":"Airport Fire Operations","alpha_tag":"TF Green FD","tg_group":"Rhode Island Airport Commission","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":58,"system_id":197,"tgid":1126,"name":"University of Rhode Island Police - Dispatch","alpha_tag":"URI PD","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":59,"system_id":197,"tgid":1131,"name":"University of Rhode Island - EMS","alpha_tag":"URI EMS","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":60,"system_id":197,"tgid":1348,"name":"St. George's School (Middletown) - Security","alpha_tag":"St George Sec","tg_group":"College/Education Security","frequency":null,"metadata":null,"tags":["Security"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":61,"system_id":197,"tgid":10228,"name":"Rhode Island School of Design - Security","alpha_tag":"RISD Secuty","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Security"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":62,"system_id":197,"tgid":10229,"name":"Providence College Security - Dispatch","alpha_tag":"PROV COLL","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Security"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":63,"system_id":197,"tgid":10230,"name":"Rhode Island College Security","alpha_tag":"RI COL SEC","tg_group":"College/Education Security","frequency":null,"metadata":null,"tags":["Security"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":64,"system_id":197,"tgid":11001,"name":"Brown University Police - Dispatch","alpha_tag":"BROWN UNIV","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":65,"system_id":197,"tgid":11002,"name":"Brown University Police - Car-to-Car","alpha_tag":"BROWN CAR","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":66,"system_id":197,"tgid":11003,"name":"Brown University Police - Tactical","alpha_tag":"BROWN TAC","tg_group":"College/Education Security","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":67,"system_id":197,"tgid":12,"name":"Metro Wide 2","alpha_tag":"METROWIDE2","tg_group":"Statewide Misc.","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":68,"system_id":197,"tgid":14,"name":"Metro Wide 4","alpha_tag":"METROWIDE4","tg_group":"Statewide Misc.","frequency":null,"metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":69,"system_id":197,"tgid":70,"name":"RI Traffic Tribunal Security","alpha_tag":"TFC TRIBUNAL","tg_group":"Statewide Misc.","frequency":null,"metadata":{"encrypted":true},"tags":["Security"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":70,"system_id":197,"tgid":168,"name":"Rhode Island Red Cross - Primary","alpha_tag":"Red Cross 1","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Other"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":71,"system_id":197,"tgid":169,"name":"Rhode Island Red Cross - Secondary","alpha_tag":"Red Cross 2","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Other"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":72,"system_id":197,"tgid":223,"name":"Statewide Nursing Homes Net","alpha_tag":"NURSING HM","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Other"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":73,"system_id":197,"tgid":243,"name":"Hospital Operations","alpha_tag":"Slater Hosp Ops","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Business"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":74,"system_id":197,"tgid":244,"name":"Slater Hospital Security","alpha_tag":"Slater Hosp Sec","tg_group":"Statewide Misc.","frequency":null,"metadata":null,"tags":["Security"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":75,"system_id":197,"tgid":1042,"name":"County Fireground","alpha_tag":"WashCo FireG","tg_group":"Washington County","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":76,"system_id":197,"tgid":1479,"name":"County Fire Station/Station","alpha_tag":"WashCo FireS","tg_group":"Washington County","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":77,"system_id":197,"tgid":1712,"name":"Fire 1 Dispatch","alpha_tag":"BarringtnFD1","tg_group":"Barrington","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":78,"system_id":197,"tgid":1713,"name":"Fire 2","alpha_tag":"BarringtnFD2","tg_group":"Barrington","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":79,"system_id":197,"tgid":1715,"name":"Police Operations","alpha_tag":"BarringtonPD 1","tg_group":"Barrington","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":80,"system_id":197,"tgid":1716,"name":"Police Secondary","alpha_tag":"BarringtonPD 2","tg_group":"Barrington","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":81,"system_id":197,"tgid":1744,"name":"Fire Operations (Patch from VHF)","alpha_tag":"Bristol FD","tg_group":"Bristol","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":82,"system_id":197,"tgid":1755,"name":"Harbormaster","alpha_tag":"Bristol Harbor","tg_group":"Bristol","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":83,"system_id":197,"tgid":2003,"name":"Police","alpha_tag":"Burrville PD","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":84,"system_id":197,"tgid":2004,"name":"Police 2","alpha_tag":"Burrvl PD2","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":85,"system_id":197,"tgid":2005,"name":"Police 3 Detectives","alpha_tag":"Burrvl PD3","tg_group":"Burrillville","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":86,"system_id":197,"tgid":2006,"name":"Police 4","alpha_tag":"Burrvl PD4","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":87,"system_id":197,"tgid":2000,"name":"Fire Misc (Ops are VHF)","alpha_tag":"Burrvl FD","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":88,"system_id":197,"tgid":2001,"name":"Fire TAC-1","alpha_tag":"Burvl FDTAC1","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":89,"system_id":197,"tgid":2009,"name":"Fire TAC-2","alpha_tag":"Burvl FDTAC2","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":90,"system_id":197,"tgid":2002,"name":"EMS Misc (Ops are VHF)","alpha_tag":"Burrvl EMS","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["EMS-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":91,"system_id":197,"tgid":2007,"name":"Town-Wide","alpha_tag":"Burrvl Town","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Multi-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":92,"system_id":197,"tgid":2008,"name":"Emergency Management","alpha_tag":"Burrvl EMA","tg_group":"Burrillville","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":93,"system_id":197,"tgid":1838,"name":"Police 1 Dispatch","alpha_tag":"CentFallsPD1","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":94,"system_id":197,"tgid":1839,"name":"Police 2","alpha_tag":"CentFallsPD2","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":95,"system_id":197,"tgid":1835,"name":"Fire Dispatch (Simulcast of UHF)","alpha_tag":"CentFalls FD 1","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":96,"system_id":197,"tgid":1836,"name":"Fireground","alpha_tag":"CentFalls FD 2","tg_group":"Central Falls","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":97,"system_id":197,"tgid":1425,"name":"Police Operations - Simulcast of UHF","alpha_tag":"CharlestownPD","tg_group":"Charlestown","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":98,"system_id":197,"tgid":1429,"name":"EMS - Linked to 151.3325","alpha_tag":"Chastown EMS","tg_group":"Charlestown","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":99,"system_id":197,"tgid":1483,"name":"Police 1 - Dispatch","alpha_tag":"Coventry PD","tg_group":"Coventry","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":100,"system_id":197,"tgid":1484,"name":"Police 2","alpha_tag":"Coventry PD2","tg_group":"Coventry","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":101,"system_id":197,"tgid":1480,"name":"Fire","alpha_tag":"Coventry FD","tg_group":"Coventry","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":102,"system_id":197,"tgid":1500,"name":"Fire - Dispatch/Operations","alpha_tag":"Cranston FD Disp","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":103,"system_id":197,"tgid":1501,"name":"Fire - Fireground 2","alpha_tag":"Cranston FD FG2","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":104,"system_id":197,"tgid":1502,"name":"Fire - Fireground 3","alpha_tag":"Cranston FD FG3","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":105,"system_id":197,"tgid":1503,"name":"Fire - Fireground 4","alpha_tag":"Cranston FD FG4","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":106,"system_id":197,"tgid":1504,"name":"Fire - Admin/Alt Fireground 5","alpha_tag":"Cranston FD Admi","tg_group":"Cranston","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":107,"system_id":197,"tgid":1520,"name":"Fire","alpha_tag":"Cumberland FD","tg_group":"Cumberland","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":108,"system_id":197,"tgid":1523,"name":"Police Secondary","alpha_tag":"Cumberland PD","tg_group":"Cumberland","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":109,"system_id":197,"tgid":1776,"name":"Fire Talk Around","alpha_tag":"E Greenwich F-TA","tg_group":"East Greenwich","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":110,"system_id":197,"tgid":1779,"name":"Police Operations","alpha_tag":"E Greenwich PD","tg_group":"East Greenwich","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":111,"system_id":197,"tgid":1869,"name":"Police 1 - Dispatch","alpha_tag":"E Prov PD 1","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":112,"system_id":197,"tgid":1872,"name":"Police 2","alpha_tag":"E Prov PD 2","tg_group":"East Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":113,"system_id":197,"tgid":1870,"name":"Police 3","alpha_tag":"E Prov PD 3","tg_group":"East Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":114,"system_id":197,"tgid":1883,"name":"Detectives","alpha_tag":"E Prov PD12","tg_group":"East Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":115,"system_id":197,"tgid":1866,"name":"Fire - Dispatch/Operations","alpha_tag":"E Prov FD 1","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":116,"system_id":197,"tgid":1867,"name":"Fire \"Channel 2\"","alpha_tag":"E Prov FD 2","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":117,"system_id":197,"tgid":1878,"name":"Fire \"Channel 3\"","alpha_tag":"E Prov FD 3","tg_group":"East Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":118,"system_id":197,"tgid":2064,"name":"Fire - Fireground","alpha_tag":"Exeter FD-G","tg_group":"Exeter","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":119,"system_id":197,"tgid":1904,"name":"Fire","alpha_tag":"Foster Fire","tg_group":"Foster","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":120,"system_id":197,"tgid":1939,"name":"Police","alpha_tag":"Glocester PD","tg_group":"Glocester","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":121,"system_id":197,"tgid":1940,"name":"Police Secondary","alpha_tag":"Glocester PD 2","tg_group":"Glocester","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":122,"system_id":197,"tgid":1410,"name":"Police","alpha_tag":"Hopkinton PD","tg_group":"Hopkinton","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":123,"system_id":197,"tgid":1100,"name":"Police 1 - Dispatch","alpha_tag":"Jamestown PD 1","tg_group":"Jamestown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":124,"system_id":197,"tgid":1101,"name":"Police 2","alpha_tag":"Jamestown PD 2","tg_group":"Jamestown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":125,"system_id":197,"tgid":1108,"name":"Fire","alpha_tag":"Jamestown FD","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":126,"system_id":197,"tgid":1120,"name":"Fireground 1","alpha_tag":"Jamestown FG 1","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":127,"system_id":197,"tgid":1121,"name":"Fireground 2","alpha_tag":"Jamestown FG 2","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":128,"system_id":197,"tgid":1114,"name":"Public Works","alpha_tag":"Jamestown DPW","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":129,"system_id":197,"tgid":1107,"name":"Town Schools","alpha_tag":"Jamestown School","tg_group":"Jamestown","frequency":null,"metadata":null,"tags":["Schools"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":130,"system_id":197,"tgid":1619,"name":"Police Operations","alpha_tag":"Johnston PD","tg_group":"Johnston","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":131,"system_id":197,"tgid":1616,"name":"Fire Operations","alpha_tag":"Johnston FD","tg_group":"Johnston","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":132,"system_id":197,"tgid":1617,"name":"Fireground","alpha_tag":"Johnston FG","tg_group":"Johnston","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":133,"system_id":197,"tgid":1683,"name":"Police F1","alpha_tag":"Lincoln Police","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":134,"system_id":197,"tgid":1684,"name":"Police F2","alpha_tag":"Lincoln Police 2","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":135,"system_id":197,"tgid":1680,"name":"Fire Dispatch","alpha_tag":"Lincoln Fire 1","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":136,"system_id":197,"tgid":1681,"name":"Fireground 2","alpha_tag":"Lincoln Fire 2","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":137,"system_id":197,"tgid":1691,"name":"Fireground 3","alpha_tag":"Lincoln Fire 3","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":138,"system_id":197,"tgid":1682,"name":"EMS","alpha_tag":"Lincoln EMS","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":139,"system_id":197,"tgid":1688,"name":"Emergency Management","alpha_tag":"Lincoln EMA","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":140,"system_id":197,"tgid":1687,"name":"Townwide","alpha_tag":"Lincoln Townwide","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":141,"system_id":197,"tgid":1692,"name":"Public Works","alpha_tag":"Lincoln DPW","tg_group":"Lincoln","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":142,"system_id":197,"tgid":1264,"name":"Police","alpha_tag":"LittleCompPD","tg_group":"Little Compton","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":143,"system_id":197,"tgid":1266,"name":"Fire","alpha_tag":"LittleCompFD","tg_group":"Little Compton","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":144,"system_id":197,"tgid":1338,"name":"Police Operations","alpha_tag":"MiddletownPD","tg_group":"Middletown","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":145,"system_id":197,"tgid":1343,"name":"Fire Operations","alpha_tag":"Middletown FD","tg_group":"Middletown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":146,"system_id":197,"tgid":1345,"name":"Townwide","alpha_tag":"MiddletownTW","tg_group":"Middletown","frequency":null,"metadata":null,"tags":["Multi-Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":147,"system_id":197,"tgid":1001,"name":"Police - Dispatch","alpha_tag":"Narrag PD 1","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":148,"system_id":197,"tgid":1002,"name":"Police - Car/Car","alpha_tag":"Narrag PD 2","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":149,"system_id":197,"tgid":1003,"name":"Police - Special Details 1/Town Beaches","alpha_tag":"Narrag PD 3","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":150,"system_id":197,"tgid":1004,"name":"Police - Special Details 2","alpha_tag":"Narrag PD 4","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":151,"system_id":197,"tgid":1005,"name":"Police - Harbormaster","alpha_tag":"Narrag PD 5","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":152,"system_id":197,"tgid":1007,"name":"Police - Detectives","alpha_tag":"Narrag PD 7","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":153,"system_id":197,"tgid":1008,"name":"Police - Detectives","alpha_tag":"Narrag PD 8","tg_group":"Narragansett","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":154,"system_id":197,"tgid":1006,"name":"Fire - Dispatch","alpha_tag":"Narrag FD","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":155,"system_id":197,"tgid":1012,"name":"Fire - Fireground 1","alpha_tag":"Narrag FDFG1","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":156,"system_id":197,"tgid":1013,"name":"Fire - Fireground 2","alpha_tag":"Narrag FDFG2","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":157,"system_id":197,"tgid":1016,"name":"Fire - Administration","alpha_tag":"Narrag FD AD","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":158,"system_id":197,"tgid":1014,"name":"Fire - EMS Ops","alpha_tag":"Narrag EMS","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":159,"system_id":197,"tgid":1017,"name":"Public Works","alpha_tag":"Narrag DPW","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":160,"system_id":197,"tgid":1010,"name":"Town Administration","alpha_tag":"Narrag TownA","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Other"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":161,"system_id":197,"tgid":1011,"name":"Townwide Interop","alpha_tag":"Narrag IOP","tg_group":"Narragansett","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":162,"system_id":197,"tgid":1376,"name":"Police","alpha_tag":"New Shore PD","tg_group":"New Shoreham","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":163,"system_id":197,"tgid":1300,"name":"Police 1 - Dispatch","alpha_tag":"Newport PD 1","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":164,"system_id":197,"tgid":1302,"name":"Police 2 - Records","alpha_tag":"Newport PD 2","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":165,"system_id":197,"tgid":1304,"name":"Police 4 - Tactical 1","alpha_tag":"Newport PD 4","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":166,"system_id":197,"tgid":1307,"name":"Police 7 - Tactical 4","alpha_tag":"Newport PD 7","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":167,"system_id":197,"tgid":1308,"name":"Police 8 - Tactical 5","alpha_tag":"Newport PD 8","tg_group":"Newport","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":168,"system_id":197,"tgid":1303,"name":"Fire Dispatch/Operations","alpha_tag":"Newport FD1","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":169,"system_id":197,"tgid":1305,"name":"Fireground Ops 1","alpha_tag":"Newport FG1","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":170,"system_id":197,"tgid":1306,"name":"Fireground Ops 2","alpha_tag":"Newport FG2","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":171,"system_id":197,"tgid":1301,"name":"Fire - Training","alpha_tag":"Newport FDT","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":172,"system_id":197,"tgid":1291,"name":"Water Department","alpha_tag":"Newport Water","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":173,"system_id":197,"tgid":1293,"name":"Public Works","alpha_tag":"Newport DPW","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":174,"system_id":197,"tgid":1297,"name":"Citywide Events","alpha_tag":"Newport Evnt","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":175,"system_id":197,"tgid":1312,"name":"Newport Citywide","alpha_tag":"Newport CW","tg_group":"Newport","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":176,"system_id":197,"tgid":1285,"name":"Police 1 - Dispatch","alpha_tag":"NKing PD 1","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":177,"system_id":197,"tgid":1286,"name":"Police 2 - Admin","alpha_tag":"NKing PD 2","tg_group":"North Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":178,"system_id":197,"tgid":1287,"name":"Police 3 - Car/Car","alpha_tag":"NKing PD 3","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":179,"system_id":197,"tgid":1280,"name":"Fire - Dispatch","alpha_tag":"NKing Fire D","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":180,"system_id":197,"tgid":1281,"name":"Fire - Fireground","alpha_tag":"NKing Fire G","tg_group":"North Kingstown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":181,"system_id":197,"tgid":1536,"name":"Police 1 - Dispatch","alpha_tag":"NorthPrv PD1","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":182,"system_id":197,"tgid":1537,"name":"Police 2 - Car/Car","alpha_tag":"NorthPrv PD2","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":183,"system_id":197,"tgid":1538,"name":"Police 3 - Tactical","alpha_tag":"NorthPrv PD3","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":184,"system_id":197,"tgid":1547,"name":"Fire Dispatch ","alpha_tag":"NorthPrv FDD","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":185,"system_id":197,"tgid":1548,"name":"Fire 2","alpha_tag":"NorthPrv Fire 2","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":186,"system_id":197,"tgid":1549,"name":"Fire 3","alpha_tag":"NorthPrv Fire 3","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":187,"system_id":197,"tgid":1550,"name":"Fire 4","alpha_tag":"NorthPrv Fire 4","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":188,"system_id":197,"tgid":1551,"name":"Fire 5","alpha_tag":"NorthPrv Fire 5","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":189,"system_id":197,"tgid":1552,"name":"Fire 6","alpha_tag":"NorthPrv Fire 6","tg_group":"North Providence","frequency":null,"metadata":{"encrypted":true},"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":190,"system_id":197,"tgid":1544,"name":"Townwide 1","alpha_tag":"NorthPrv TownW 1","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":191,"system_id":197,"tgid":1545,"name":"Townwide 2","alpha_tag":"NorthPrv TownW 2","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Interop"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":192,"system_id":197,"tgid":1554,"name":"Public Works","alpha_tag":"NorthPrv DPW","tg_group":"North Providence","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":193,"system_id":197,"tgid":1971,"name":"Police","alpha_tag":"N Smithfd PD","tg_group":"North Smithfield","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":194,"system_id":197,"tgid":1968,"name":"Fire Dispatch/Operations","alpha_tag":"N Smithfield FD","tg_group":"North Smithfield","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":195,"system_id":197,"tgid":1969,"name":"Fire Secondary","alpha_tag":"N Smithfield FD2","tg_group":"North Smithfield","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":196,"system_id":197,"tgid":1981,"name":"Fireground","alpha_tag":"N Smithfield FD3","tg_group":"North Smithfield","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":197,"system_id":197,"tgid":1440,"name":"Fire - Operations","alpha_tag":"Pawtucket FD 1","tg_group":"Pawtucket","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":198,"system_id":197,"tgid":1441,"name":"Fireground","alpha_tag":"Pawtucket FG","tg_group":"Pawtucket","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":199,"system_id":197,"tgid":1442,"name":"EMS Tac","alpha_tag":"Pawtucket EMSTac","tg_group":"Pawtucket","frequency":null,"metadata":null,"tags":["EMS-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":200,"system_id":197,"tgid":1248,"name":"Police","alpha_tag":"PortsmouthPD","tg_group":"Portsmouth","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":201,"system_id":197,"tgid":1253,"name":"Fire Dispatch (Patch to VHF Primary)","alpha_tag":"Portsmouth FD","tg_group":"Portsmouth","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":202,"system_id":197,"tgid":1255,"name":"Fireground","alpha_tag":"Portsmouth FG","tg_group":"Portsmouth","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":203,"system_id":197,"tgid":1262,"name":"Island Fire Dispatch","alpha_tag":"Prudence Isl FD","tg_group":"Portsmouth","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":204,"system_id":197,"tgid":10000,"name":"Police - All Call - Emergency Broadcasts","alpha_tag":"PPD ATG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":205,"system_id":197,"tgid":10001,"name":"Police 1 - Dispatch","alpha_tag":"PPD CH 1","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":206,"system_id":197,"tgid":10002,"name":"Police 2","alpha_tag":"PPD CH 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":207,"system_id":197,"tgid":10003,"name":"Police 3","alpha_tag":"PPD CH 3","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":208,"system_id":197,"tgid":10004,"name":"Police 4","alpha_tag":"PPD CH-4","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":209,"system_id":197,"tgid":10005,"name":"Police 5 -Detectives 1","alpha_tag":"PPD DETEC 1","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":210,"system_id":197,"tgid":10006,"name":"Police 6 - Car-to-Car","alpha_tag":"PPD T/A","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":211,"system_id":197,"tgid":10007,"name":"Police 7 - Narcotics 1","alpha_tag":"PPD NARC 1","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":212,"system_id":197,"tgid":10008,"name":"Police 8 - Narcotics 2","alpha_tag":"PPD NARC 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":213,"system_id":197,"tgid":10009,"name":"Police 9 - Detectives 2","alpha_tag":"PPD DETEC 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":214,"system_id":197,"tgid":10010,"name":"Police 10 - Special Details 1","alpha_tag":"PPD DETAIL 1","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":215,"system_id":197,"tgid":10011,"name":"Police 11 - Special Details 2","alpha_tag":"PPD DETAIL 2","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":216,"system_id":197,"tgid":10012,"name":"Police 12 - Corrections Security","alpha_tag":"PPD CORR SEC","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":217,"system_id":197,"tgid":10013,"name":"Police 13 - Special Response Unit","alpha_tag":"PPD SRU","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":218,"system_id":197,"tgid":10014,"name":"Police 14 - Administration","alpha_tag":"PPD ADMIN","tg_group":"Providence (City)","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":219,"system_id":197,"tgid":10100,"name":"Fire All Call - Emergency Broadcasts","alpha_tag":"PROV FD ATG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":220,"system_id":197,"tgid":10101,"name":"Fire Dispatch","alpha_tag":"PFD DISPATCH","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":221,"system_id":197,"tgid":10107,"name":"Fireground 2","alpha_tag":"PFD CH-2 FG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":222,"system_id":197,"tgid":10108,"name":"Fireground 3","alpha_tag":"PFD CH-3 FG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":223,"system_id":197,"tgid":10109,"name":"Fireground 4","alpha_tag":"PFD CH-4 FG","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":224,"system_id":197,"tgid":10102,"name":"Fire 5","alpha_tag":"PFD CH-5","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":225,"system_id":197,"tgid":10103,"name":"Fire 6","alpha_tag":"PFD CH-6","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":226,"system_id":197,"tgid":10104,"name":"Fire 7","alpha_tag":"PFD CH-7","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":227,"system_id":197,"tgid":10110,"name":"Fire - Mutual Aid 1","alpha_tag":"PFD M/A 1","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":228,"system_id":197,"tgid":10111,"name":"Fire - Mutual Aid 2","alpha_tag":"PFD M/A 2","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":229,"system_id":197,"tgid":10112,"name":"Fire - Mutual Aid 3","alpha_tag":"PFD M/A 3","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":230,"system_id":197,"tgid":10113,"name":"Fireground 8","alpha_tag":"PFD Fireground 8","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":231,"system_id":197,"tgid":10105,"name":"Fire - Administration","alpha_tag":"PFD ADMIN","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":232,"system_id":197,"tgid":10106,"name":"Fire - Communications","alpha_tag":"PFD COMM","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":233,"system_id":197,"tgid":10207,"name":"Public Works","alpha_tag":"PROV DPW","tg_group":"Providence (City)","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":234,"system_id":197,"tgid":2035,"name":"Police","alpha_tag":"Richmond PD","tg_group":"Richmond","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":235,"system_id":197,"tgid":2042,"name":"Chariho Regional High School","alpha_tag":"Chariho Reg HS","tg_group":"Richmond","frequency":null,"metadata":null,"tags":["Schools"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":236,"system_id":197,"tgid":1460,"name":"Police","alpha_tag":"Scituate PD","tg_group":"Scituate","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":237,"system_id":197,"tgid":1463,"name":"Fire Operations","alpha_tag":"Scituate FD","tg_group":"Scituate","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":238,"system_id":197,"tgid":1651,"name":"Police Operations","alpha_tag":"SmithfieldPD","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":239,"system_id":197,"tgid":1652,"name":"Police Secondary","alpha_tag":"Smfld PD 2","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":240,"system_id":197,"tgid":1653,"name":"Police Detectives","alpha_tag":"Smfld PD Det","tg_group":"Smithfield","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":241,"system_id":197,"tgid":1654,"name":"Police Admin","alpha_tag":"Smfld PD Adm","tg_group":"Smithfield","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":242,"system_id":197,"tgid":1661,"name":"Police Details","alpha_tag":"Smfld PD Dtl","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":243,"system_id":197,"tgid":1648,"name":"Fire - Fireground","alpha_tag":"SmithfieldFD","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":244,"system_id":197,"tgid":1655,"name":"Town-Wide","alpha_tag":"Smfld Town","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Multi-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":245,"system_id":197,"tgid":1657,"name":"Emergency Management","alpha_tag":"Smfld EMA","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Emergency Ops"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":246,"system_id":197,"tgid":1660,"name":"Public Works","alpha_tag":"Smfld DPW","tg_group":"Smithfield","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":247,"system_id":197,"tgid":1225,"name":"Police 1 - Dispatch","alpha_tag":"SKing PD 1","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":248,"system_id":197,"tgid":1226,"name":"Police 2 - Car/Car","alpha_tag":"SKing PD 2","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":249,"system_id":197,"tgid":1235,"name":"Police 3 - Tactical","alpha_tag":"SKing PD 3","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":250,"system_id":197,"tgid":1236,"name":"Police 5 - Tactical","alpha_tag":"SKing PD 5","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":251,"system_id":197,"tgid":1232,"name":"Fire - UHF Simulcast","alpha_tag":"SKing FD Lnk","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":252,"system_id":197,"tgid":1240,"name":"Fire - Detail","alpha_tag":"SKing Fire D","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":253,"system_id":197,"tgid":1227,"name":"Union Fire District - Fireground 1","alpha_tag":"UnionFD FG 1","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":254,"system_id":197,"tgid":1237,"name":"Union Fire District - Fireground 2","alpha_tag":"UnionFD FG 2","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":255,"system_id":197,"tgid":1026,"name":"Union Fire District - Special Events","alpha_tag":"UnionFD Evnt","tg_group":"South Kingstown","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":256,"system_id":197,"tgid":1015,"name":"EMS","alpha_tag":"SKing EMS","tg_group":"South Kingstown","frequency":null,"metadata":{"encrypted":true},"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":257,"system_id":197,"tgid":1316,"name":"Police (Simulcast 482.9625)","alpha_tag":"Tiverton PD","tg_group":"Tiverton","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":258,"system_id":197,"tgid":1315,"name":"Fire (Simulcast 471.7875)","alpha_tag":"Tiverton FD","tg_group":"Tiverton","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":259,"system_id":197,"tgid":1162,"name":"Fire","alpha_tag":"Warwick FD","tg_group":"Warwick","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":260,"system_id":197,"tgid":1170,"name":"Fireground","alpha_tag":"Warwick FG","tg_group":"Warwick","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":261,"system_id":197,"tgid":1805,"name":"Police","alpha_tag":"W Greenwh PD","tg_group":"West Greenwich","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":262,"system_id":197,"tgid":1806,"name":"Police Secondary","alpha_tag":"W GreenwichPD2","tg_group":"West Greenwich","frequency":null,"metadata":null,"tags":["Law Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":263,"system_id":197,"tgid":1208,"name":"Fire Operations","alpha_tag":"W Warwick FD","tg_group":"West Warwick","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":264,"system_id":197,"tgid":1050,"name":"Police 1 - Dispatch","alpha_tag":"Westerly PD1","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":265,"system_id":197,"tgid":1051,"name":"Police 2","alpha_tag":"Westerly PD2","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":266,"system_id":197,"tgid":1052,"name":"Police 3","alpha_tag":"Westerly PD3","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":267,"system_id":197,"tgid":1053,"name":"Police 4","alpha_tag":"Westerly PD4","tg_group":"Westerly","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":268,"system_id":197,"tgid":1054,"name":"Police 5 - Reserve Officers","alpha_tag":"Westerly PD5","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":269,"system_id":197,"tgid":1064,"name":"Police 6 - Traffic Division","alpha_tag":"Westerly PD6","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":270,"system_id":197,"tgid":1063,"name":"Fire Operations","alpha_tag":"Westerly FD","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":271,"system_id":197,"tgid":1072,"name":"Police/Fire/EMS Ops","alpha_tag":"Westerly PFE","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["Multi-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":272,"system_id":197,"tgid":1082,"name":"EMS Operations","alpha_tag":"Westerly EMS ","tg_group":"Westerly","frequency":null,"metadata":null,"tags":["EMS Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":273,"system_id":197,"tgid":1363,"name":"Police 1 - Dispatch","alpha_tag":"Woonskt PD 1","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Law Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":274,"system_id":197,"tgid":1364,"name":"Police 2","alpha_tag":"Woonskt PD 2","tg_group":"Woonsocket","frequency":null,"metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":275,"system_id":197,"tgid":1360,"name":"Fire Dispatch - Operations","alpha_tag":"Woonsocket FD D","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Fire-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":276,"system_id":197,"tgid":1361,"name":"Fire Secondary","alpha_tag":"Woonsocket FD 2","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Fire Dispatch"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":277,"system_id":197,"tgid":1354,"name":"Fire - Fireground 3","alpha_tag":"Woonskt FD 3","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Fire-Tac"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":278,"system_id":197,"tgid":1367,"name":"Citywide","alpha_tag":"Woonskt City","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Multi-Talk"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":279,"system_id":197,"tgid":1368,"name":"Public Works - Streets","alpha_tag":"Woonsocket PW","tg_group":"Woonsocket","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":280,"system_id":197,"tgid":1,"name":"RISCON Radio Technicians","alpha_tag":"Radio Techs","tg_group":"Radio Technicians","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":281,"system_id":197,"tgid":10125,"name":"RISCON Radio Technicians","alpha_tag":"Radio Techs","tg_group":"Radio Technicians","frequency":null,"metadata":null,"tags":["Public Works"],"alert":true,"alert_rules":null,"weight":1,"ignored":false,"system":{"id":197,"name":"RISCON"},"learned":false}] \ No newline at end of file +[{"systemId":197,"tgid":2,"name":"Intercity Fire","alphaTag":"Intercity FD","tgGroup":"Statewide Mutual Aid/Intersystem","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":1,"systemId":197,"tgid":3,"name":"Intercity Police","alphaTag":"Intercity PD","tgGroup":"Statewide Mutual Aid/Intersystem","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":2,"systemId":197,"tgid":21,"name":"North Dispatch ","alphaTag":"RISP N Disp","tgGroup":"State Police - District A (North)","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":3,"systemId":197,"tgid":22,"name":"North Car-to-Car/Information","alphaTag":"RISP N Car","tgGroup":"State Police - District A (North)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":4,"systemId":197,"tgid":24,"name":"North Tactical Ops 1","alphaTag":"RISP N Tac 1","tgGroup":"State Police - District A (North)","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":5,"systemId":197,"tgid":23,"name":"North Tactical Ops 2","alphaTag":"RISP N Tac 2","tgGroup":"State Police - District A (North)","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":6,"systemId":197,"tgid":25,"name":"South Dispatch ","alphaTag":"RISP S Disp","tgGroup":"State Police - District B (South)","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":7,"systemId":197,"tgid":27,"name":"South Car-to-Car/Information","alphaTag":"RISP S Car","tgGroup":"State Police - District B (South)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":8,"systemId":197,"tgid":16,"name":"State Fire Marshall","alphaTag":"State FMO","tgGroup":"Statewide Fire","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":9,"systemId":197,"tgid":1038,"name":"Northern Rhode Island Fire Chiefs","alphaTag":"NRI Fire Chi","tgGroup":"Statewide Fire","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":10,"systemId":197,"tgid":1041,"name":"Southern Rhode Island Fire Chiefs","alphaTag":"SRI Fire Chi","tgGroup":"Statewide Fire","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":11,"systemId":197,"tgid":1314,"name":"Tanker Taskforce 1","alphaTag":"Tanker TF 1","tgGroup":"Statewide Fire","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":12,"systemId":197,"tgid":194,"name":"Lifepact Ambulance (Statewide)","alphaTag":"Lifepact Amb","tgGroup":"Statewide EMS and Hospitals","tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":13,"systemId":197,"tgid":212,"name":"Fatima St Josephs","alphaTag":"Fatima-St Joes","tgGroup":"Statewide EMS and Hospitals","tags":["Business"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":14,"systemId":197,"tgid":220,"name":"Health 1","alphaTag":"Health 1","tgGroup":"Statewide EMS and Hospitals","metadata":{"encrypted":true},"tags":["EMS-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":15,"systemId":197,"tgid":221,"name":"Health 2","alphaTag":"Health 2","tgGroup":"Statewide EMS and Hospitals","metadata":{"encrypted":true},"tags":["EMS-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":16,"systemId":197,"tgid":222,"name":"Department of Health - Statewide","alphaTag":"Dept of HealthSW","tgGroup":"Statewide EMS and Hospitals","tags":["EMS-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":17,"systemId":197,"tgid":228,"name":"DMAT South","alphaTag":"DMAT South","tgGroup":"Statewide EMS and Hospitals","metadata":{"encrypted":true},"tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":18,"systemId":197,"tgid":232,"name":"Life Span Net 1","alphaTag":"Life Span 1","tgGroup":"Statewide EMS and Hospitals","tags":["EMS-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":19,"systemId":197,"tgid":234,"name":"RI Hospital Operations","alphaTag":"RI Hosp Ops","tgGroup":"Statewide EMS and Hospitals","tags":["Business"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":20,"systemId":197,"tgid":120,"name":"Law Enforcement Operations","alphaTag":"DEM PD Ops","tgGroup":"Department of Environmental Management","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":21,"systemId":197,"tgid":122,"name":"Law Enforcement Police","alphaTag":"DEM Police","tgGroup":"Department of Environmental Management","tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":22,"systemId":197,"tgid":10,"name":"Emergency Management Agency 1","alphaTag":"EMA-1","tgGroup":"Emergency Management Agency","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":23,"systemId":197,"tgid":20,"name":"Emergency Management Agency","alphaTag":"EMA","tgGroup":"Emergency Management Agency","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":24,"systemId":197,"tgid":4,"name":"Wide Area 3","alphaTag":"Wide Area 3","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":25,"systemId":197,"tgid":5,"name":"Wide Area 4","alphaTag":"Wide Area 4","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":26,"systemId":197,"tgid":6,"name":"Wide Area 5","alphaTag":"Wide Area 5","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":27,"systemId":197,"tgid":7,"name":"Wide Area 6","alphaTag":"Wide Area 6","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":28,"systemId":197,"tgid":1018,"name":"Southwide CH-1","alphaTag":"SOUTHWIDE 1","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":29,"systemId":197,"tgid":1019,"name":"Southwide CH-2","alphaTag":"SOUTHWIDE 2","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":30,"systemId":197,"tgid":1022,"name":"Wide Area 7","alphaTag":"WIDE AREA 7","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":31,"systemId":197,"tgid":1023,"name":"Wide Area 8","alphaTag":"WIDE AREA 8","tgGroup":"Statewide Area/Events","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":32,"systemId":197,"tgid":1025,"name":"Inland Marine Interop","alphaTag":"Inland Marine IO","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":33,"systemId":197,"tgid":1037,"name":"Southside CH 5","alphaTag":"SOUTHSIDE 5","tgGroup":"Statewide Area/Events","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":34,"systemId":197,"tgid":1173,"name":"North Wide 1","alphaTag":"NORTHWIDE1","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":35,"systemId":197,"tgid":1174,"name":"North Wide 2","alphaTag":"NORTHWIDE2","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":36,"systemId":197,"tgid":1177,"name":"North Wide 5","alphaTag":"NORTHWIDE5","tgGroup":"Statewide Area/Events","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":37,"systemId":197,"tgid":1185,"name":"Metro Wide 1","alphaTag":"METROWIDE1","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":38,"systemId":197,"tgid":1186,"name":"Metro Wide 2","alphaTag":"METROWIDE2","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":39,"systemId":197,"tgid":1187,"name":"Metro Wide 3","alphaTag":"METROWIDE3","tgGroup":"Statewide Area/Events","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":40,"systemId":197,"tgid":1335,"name":"East Wide 1","alphaTag":"EASTWIDE 1","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":41,"systemId":197,"tgid":1336,"name":"East Wide 2","alphaTag":"EASTWIDE 2","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":42,"systemId":197,"tgid":1337,"name":"East Wide 3","alphaTag":"EASTWIDE 3","tgGroup":"Statewide Area/Events","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":43,"systemId":197,"tgid":11186,"name":"Metro Wide 2","alphaTag":"METROWIDE2","tgGroup":"Statewide Area/Events","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":44,"systemId":197,"tgid":1033,"name":"Tanker Taskforce ","alphaTag":"TANK TF","tgGroup":"Statewide Emergency Response","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":45,"systemId":197,"tgid":1034,"name":"Hazmat 1","alphaTag":"HZT DC1","tgGroup":"Statewide Emergency Response","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":46,"systemId":197,"tgid":1035,"name":"Hazmat 2","alphaTag":"HZT DC2","tgGroup":"Statewide Emergency Response","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":47,"systemId":197,"tgid":176,"name":"Department of Transportation - Primary","alphaTag":"RIDOT Primary","tgGroup":"Department of Transportation","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":48,"systemId":197,"tgid":4421,"name":"Newport Pell Bridge Operations","alphaTag":"RITBA - Pell Bdg","tgGroup":"Tunnel and Bridge Authority","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":49,"systemId":197,"tgid":274,"name":"Providence VA Police","alphaTag":"VA Police","tgGroup":"Federal","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":50,"systemId":197,"tgid":186,"name":"Rhode Island Public Transit Auth.","alphaTag":"RIPTA","tgGroup":"RIPTA","metadata":{"encrypted":true},"tags":["Transportation"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":51,"systemId":197,"tgid":187,"name":"Rhode Island Public Transit Auth.","alphaTag":"RIPTA","tgGroup":"RIPTA","tags":["Transportation"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":52,"systemId":197,"tgid":188,"name":"Rhode Island Public Transit Auth.","alphaTag":"RIPTA","tgGroup":"RIPTA","tags":["Transportation"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":53,"systemId":197,"tgid":189,"name":"Rhode Island Public Transit Auth.","alphaTag":"RIPTA","tgGroup":"RIPTA","tags":["Transportation"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":54,"systemId":197,"tgid":190,"name":"Rhode Island Public Transit. Auth.","alphaTag":"RIPTA","tgGroup":"RIPTA","tags":["Transportation"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":55,"systemId":197,"tgid":304,"name":"Fire Operations","alphaTag":"Quonset ANGB FD","tgGroup":"Quonset ANGB","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":56,"systemId":197,"tgid":17,"name":"Airport Police Operations","alphaTag":"TF Green PD","tgGroup":"Rhode Island Airport Commission","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":57,"systemId":197,"tgid":19,"name":"Airport Fire Operations","alphaTag":"TF Green FD","tgGroup":"Rhode Island Airport Commission","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":58,"systemId":197,"tgid":1126,"name":"University of Rhode Island Police - Dispatch","alphaTag":"URI PD","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":59,"systemId":197,"tgid":1131,"name":"University of Rhode Island - EMS","alphaTag":"URI EMS","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":60,"systemId":197,"tgid":1348,"name":"St. George's School (Middletown) - Security","alphaTag":"St George Sec","tgGroup":"College/Education Security","tags":["Security"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":61,"systemId":197,"tgid":10228,"name":"Rhode Island School of Design - Security","alphaTag":"RISD Secuty","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["Security"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":62,"systemId":197,"tgid":10229,"name":"Providence College Security - Dispatch","alphaTag":"PROV COLL","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["Security"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":63,"systemId":197,"tgid":10230,"name":"Rhode Island College Security","alphaTag":"RI COL SEC","tgGroup":"College/Education Security","tags":["Security"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":64,"systemId":197,"tgid":11001,"name":"Brown University Police - Dispatch","alphaTag":"BROWN UNIV","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":65,"systemId":197,"tgid":11002,"name":"Brown University Police - Car-to-Car","alphaTag":"BROWN CAR","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":66,"systemId":197,"tgid":11003,"name":"Brown University Police - Tactical","alphaTag":"BROWN TAC","tgGroup":"College/Education Security","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":67,"systemId":197,"tgid":12,"name":"Metro Wide 2","alphaTag":"METROWIDE2","tgGroup":"Statewide Misc.","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":68,"systemId":197,"tgid":14,"name":"Metro Wide 4","alphaTag":"METROWIDE4","tgGroup":"Statewide Misc.","metadata":{"encrypted":true},"tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":69,"systemId":197,"tgid":70,"name":"RI Traffic Tribunal Security","alphaTag":"TFC TRIBUNAL","tgGroup":"Statewide Misc.","metadata":{"encrypted":true},"tags":["Security"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":70,"systemId":197,"tgid":168,"name":"Rhode Island Red Cross - Primary","alphaTag":"Red Cross 1","tgGroup":"Statewide Misc.","tags":["Other"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":71,"systemId":197,"tgid":169,"name":"Rhode Island Red Cross - Secondary","alphaTag":"Red Cross 2","tgGroup":"Statewide Misc.","tags":["Other"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":72,"systemId":197,"tgid":223,"name":"Statewide Nursing Homes Net","alphaTag":"NURSING HM","tgGroup":"Statewide Misc.","tags":["Other"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":73,"systemId":197,"tgid":243,"name":"Hospital Operations","alphaTag":"Slater Hosp Ops","tgGroup":"Statewide Misc.","tags":["Business"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":74,"systemId":197,"tgid":244,"name":"Slater Hospital Security","alphaTag":"Slater Hosp Sec","tgGroup":"Statewide Misc.","tags":["Security"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":75,"systemId":197,"tgid":1042,"name":"County Fireground","alphaTag":"WashCo FireG","tgGroup":"Washington County","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":76,"systemId":197,"tgid":1479,"name":"County Fire Station/Station","alphaTag":"WashCo FireS","tgGroup":"Washington County","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":77,"systemId":197,"tgid":1712,"name":"Fire 1 Dispatch","alphaTag":"BarringtnFD1","tgGroup":"Barrington","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":78,"systemId":197,"tgid":1713,"name":"Fire 2","alphaTag":"BarringtnFD2","tgGroup":"Barrington","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":79,"systemId":197,"tgid":1715,"name":"Police Operations","alphaTag":"BarringtonPD 1","tgGroup":"Barrington","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":80,"systemId":197,"tgid":1716,"name":"Police Secondary","alphaTag":"BarringtonPD 2","tgGroup":"Barrington","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":81,"systemId":197,"tgid":1744,"name":"Fire Operations (Patch from VHF)","alphaTag":"Bristol FD","tgGroup":"Bristol","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":82,"systemId":197,"tgid":1755,"name":"Harbormaster","alphaTag":"Bristol Harbor","tgGroup":"Bristol","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":83,"systemId":197,"tgid":2003,"name":"Police","alphaTag":"Burrville PD","tgGroup":"Burrillville","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":84,"systemId":197,"tgid":2004,"name":"Police 2","alphaTag":"Burrvl PD2","tgGroup":"Burrillville","tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":85,"systemId":197,"tgid":2005,"name":"Police 3 Detectives","alphaTag":"Burrvl PD3","tgGroup":"Burrillville","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":86,"systemId":197,"tgid":2006,"name":"Police 4","alphaTag":"Burrvl PD4","tgGroup":"Burrillville","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":87,"systemId":197,"tgid":2000,"name":"Fire Misc (Ops are VHF)","alphaTag":"Burrvl FD","tgGroup":"Burrillville","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":88,"systemId":197,"tgid":2001,"name":"Fire TAC-1","alphaTag":"Burvl FDTAC1","tgGroup":"Burrillville","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":89,"systemId":197,"tgid":2009,"name":"Fire TAC-2","alphaTag":"Burvl FDTAC2","tgGroup":"Burrillville","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":90,"systemId":197,"tgid":2002,"name":"EMS Misc (Ops are VHF)","alphaTag":"Burrvl EMS","tgGroup":"Burrillville","tags":["EMS-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":91,"systemId":197,"tgid":2007,"name":"Town-Wide","alphaTag":"Burrvl Town","tgGroup":"Burrillville","tags":["Multi-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":92,"systemId":197,"tgid":2008,"name":"Emergency Management","alphaTag":"Burrvl EMA","tgGroup":"Burrillville","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":93,"systemId":197,"tgid":1838,"name":"Police 1 Dispatch","alphaTag":"CentFallsPD1","tgGroup":"Central Falls","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":94,"systemId":197,"tgid":1839,"name":"Police 2","alphaTag":"CentFallsPD2","tgGroup":"Central Falls","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":95,"systemId":197,"tgid":1835,"name":"Fire Dispatch (Simulcast of UHF)","alphaTag":"CentFalls FD 1","tgGroup":"Central Falls","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":96,"systemId":197,"tgid":1836,"name":"Fireground","alphaTag":"CentFalls FD 2","tgGroup":"Central Falls","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":97,"systemId":197,"tgid":1425,"name":"Police Operations - Simulcast of UHF","alphaTag":"CharlestownPD","tgGroup":"Charlestown","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":98,"systemId":197,"tgid":1429,"name":"EMS - Linked to 151.3325","alphaTag":"Chastown EMS","tgGroup":"Charlestown","tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":99,"systemId":197,"tgid":1483,"name":"Police 1 - Dispatch","alphaTag":"Coventry PD","tgGroup":"Coventry","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":100,"systemId":197,"tgid":1484,"name":"Police 2","alphaTag":"Coventry PD2","tgGroup":"Coventry","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":101,"systemId":197,"tgid":1480,"name":"Fire","alphaTag":"Coventry FD","tgGroup":"Coventry","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":102,"systemId":197,"tgid":1500,"name":"Fire - Dispatch/Operations","alphaTag":"Cranston FD Disp","tgGroup":"Cranston","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":103,"systemId":197,"tgid":1501,"name":"Fire - Fireground 2","alphaTag":"Cranston FD FG2","tgGroup":"Cranston","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":104,"systemId":197,"tgid":1502,"name":"Fire - Fireground 3","alphaTag":"Cranston FD FG3","tgGroup":"Cranston","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":105,"systemId":197,"tgid":1503,"name":"Fire - Fireground 4","alphaTag":"Cranston FD FG4","tgGroup":"Cranston","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":106,"systemId":197,"tgid":1504,"name":"Fire - Admin/Alt Fireground 5","alphaTag":"Cranston FD Admi","tgGroup":"Cranston","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":107,"systemId":197,"tgid":1520,"name":"Fire","alphaTag":"Cumberland FD","tgGroup":"Cumberland","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":108,"systemId":197,"tgid":1523,"name":"Police Secondary","alphaTag":"Cumberland PD","tgGroup":"Cumberland","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":109,"systemId":197,"tgid":1776,"name":"Fire Talk Around","alphaTag":"E Greenwich F-TA","tgGroup":"East Greenwich","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":110,"systemId":197,"tgid":1779,"name":"Police Operations","alphaTag":"E Greenwich PD","tgGroup":"East Greenwich","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":111,"systemId":197,"tgid":1869,"name":"Police 1 - Dispatch","alphaTag":"E Prov PD 1","tgGroup":"East Providence","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":112,"systemId":197,"tgid":1872,"name":"Police 2","alphaTag":"E Prov PD 2","tgGroup":"East Providence","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":113,"systemId":197,"tgid":1870,"name":"Police 3","alphaTag":"E Prov PD 3","tgGroup":"East Providence","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":114,"systemId":197,"tgid":1883,"name":"Detectives","alphaTag":"E Prov PD12","tgGroup":"East Providence","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":115,"systemId":197,"tgid":1866,"name":"Fire - Dispatch/Operations","alphaTag":"E Prov FD 1","tgGroup":"East Providence","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":116,"systemId":197,"tgid":1867,"name":"Fire \"Channel 2\"","alphaTag":"E Prov FD 2","tgGroup":"East Providence","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":117,"systemId":197,"tgid":1878,"name":"Fire \"Channel 3\"","alphaTag":"E Prov FD 3","tgGroup":"East Providence","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":118,"systemId":197,"tgid":2064,"name":"Fire - Fireground","alphaTag":"Exeter FD-G","tgGroup":"Exeter","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":119,"systemId":197,"tgid":1904,"name":"Fire","alphaTag":"Foster Fire","tgGroup":"Foster","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":120,"systemId":197,"tgid":1939,"name":"Police","alphaTag":"Glocester PD","tgGroup":"Glocester","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":121,"systemId":197,"tgid":1940,"name":"Police Secondary","alphaTag":"Glocester PD 2","tgGroup":"Glocester","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":122,"systemId":197,"tgid":1410,"name":"Police","alphaTag":"Hopkinton PD","tgGroup":"Hopkinton","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":123,"systemId":197,"tgid":1100,"name":"Police 1 - Dispatch","alphaTag":"Jamestown PD 1","tgGroup":"Jamestown","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":124,"systemId":197,"tgid":1101,"name":"Police 2","alphaTag":"Jamestown PD 2","tgGroup":"Jamestown","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":125,"systemId":197,"tgid":1108,"name":"Fire","alphaTag":"Jamestown FD","tgGroup":"Jamestown","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":126,"systemId":197,"tgid":1120,"name":"Fireground 1","alphaTag":"Jamestown FG 1","tgGroup":"Jamestown","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":127,"systemId":197,"tgid":1121,"name":"Fireground 2","alphaTag":"Jamestown FG 2","tgGroup":"Jamestown","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":128,"systemId":197,"tgid":1114,"name":"Public Works","alphaTag":"Jamestown DPW","tgGroup":"Jamestown","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":129,"systemId":197,"tgid":1107,"name":"Town Schools","alphaTag":"Jamestown School","tgGroup":"Jamestown","tags":["Schools"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":130,"systemId":197,"tgid":1619,"name":"Police Operations","alphaTag":"Johnston PD","tgGroup":"Johnston","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":131,"systemId":197,"tgid":1616,"name":"Fire Operations","alphaTag":"Johnston FD","tgGroup":"Johnston","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":132,"systemId":197,"tgid":1617,"name":"Fireground","alphaTag":"Johnston FG","tgGroup":"Johnston","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":133,"systemId":197,"tgid":1683,"name":"Police F1","alphaTag":"Lincoln Police","tgGroup":"Lincoln","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":134,"systemId":197,"tgid":1684,"name":"Police F2","alphaTag":"Lincoln Police 2","tgGroup":"Lincoln","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":135,"systemId":197,"tgid":1680,"name":"Fire Dispatch","alphaTag":"Lincoln Fire 1","tgGroup":"Lincoln","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":136,"systemId":197,"tgid":1681,"name":"Fireground 2","alphaTag":"Lincoln Fire 2","tgGroup":"Lincoln","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":137,"systemId":197,"tgid":1691,"name":"Fireground 3","alphaTag":"Lincoln Fire 3","tgGroup":"Lincoln","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":138,"systemId":197,"tgid":1682,"name":"EMS","alphaTag":"Lincoln EMS","tgGroup":"Lincoln","tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":139,"systemId":197,"tgid":1688,"name":"Emergency Management","alphaTag":"Lincoln EMA","tgGroup":"Lincoln","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":140,"systemId":197,"tgid":1687,"name":"Townwide","alphaTag":"Lincoln Townwide","tgGroup":"Lincoln","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":141,"systemId":197,"tgid":1692,"name":"Public Works","alphaTag":"Lincoln DPW","tgGroup":"Lincoln","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":142,"systemId":197,"tgid":1264,"name":"Police","alphaTag":"LittleCompPD","tgGroup":"Little Compton","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":143,"systemId":197,"tgid":1266,"name":"Fire","alphaTag":"LittleCompFD","tgGroup":"Little Compton","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":144,"systemId":197,"tgid":1338,"name":"Police Operations","alphaTag":"MiddletownPD","tgGroup":"Middletown","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":145,"systemId":197,"tgid":1343,"name":"Fire Operations","alphaTag":"Middletown FD","tgGroup":"Middletown","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":146,"systemId":197,"tgid":1345,"name":"Townwide","alphaTag":"MiddletownTW","tgGroup":"Middletown","tags":["Multi-Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":147,"systemId":197,"tgid":1001,"name":"Police - Dispatch","alphaTag":"Narrag PD 1","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":148,"systemId":197,"tgid":1002,"name":"Police - Car/Car","alphaTag":"Narrag PD 2","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":149,"systemId":197,"tgid":1003,"name":"Police - Special Details 1/Town Beaches","alphaTag":"Narrag PD 3","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":150,"systemId":197,"tgid":1004,"name":"Police - Special Details 2","alphaTag":"Narrag PD 4","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":151,"systemId":197,"tgid":1005,"name":"Police - Harbormaster","alphaTag":"Narrag PD 5","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":152,"systemId":197,"tgid":1007,"name":"Police - Detectives","alphaTag":"Narrag PD 7","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":153,"systemId":197,"tgid":1008,"name":"Police - Detectives","alphaTag":"Narrag PD 8","tgGroup":"Narragansett","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":154,"systemId":197,"tgid":1006,"name":"Fire - Dispatch","alphaTag":"Narrag FD","tgGroup":"Narragansett","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":155,"systemId":197,"tgid":1012,"name":"Fire - Fireground 1","alphaTag":"Narrag FDFG1","tgGroup":"Narragansett","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":156,"systemId":197,"tgid":1013,"name":"Fire - Fireground 2","alphaTag":"Narrag FDFG2","tgGroup":"Narragansett","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":157,"systemId":197,"tgid":1016,"name":"Fire - Administration","alphaTag":"Narrag FD AD","tgGroup":"Narragansett","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":158,"systemId":197,"tgid":1014,"name":"Fire - EMS Ops","alphaTag":"Narrag EMS","tgGroup":"Narragansett","tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":159,"systemId":197,"tgid":1017,"name":"Public Works","alphaTag":"Narrag DPW","tgGroup":"Narragansett","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":160,"systemId":197,"tgid":1010,"name":"Town Administration","alphaTag":"Narrag TownA","tgGroup":"Narragansett","tags":["Other"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":161,"systemId":197,"tgid":1011,"name":"Townwide Interop","alphaTag":"Narrag IOP","tgGroup":"Narragansett","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":162,"systemId":197,"tgid":1376,"name":"Police","alphaTag":"New Shore PD","tgGroup":"New Shoreham","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":163,"systemId":197,"tgid":1300,"name":"Police 1 - Dispatch","alphaTag":"Newport PD 1","tgGroup":"Newport","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":164,"systemId":197,"tgid":1302,"name":"Police 2 - Records","alphaTag":"Newport PD 2","tgGroup":"Newport","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":165,"systemId":197,"tgid":1304,"name":"Police 4 - Tactical 1","alphaTag":"Newport PD 4","tgGroup":"Newport","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":166,"systemId":197,"tgid":1307,"name":"Police 7 - Tactical 4","alphaTag":"Newport PD 7","tgGroup":"Newport","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":167,"systemId":197,"tgid":1308,"name":"Police 8 - Tactical 5","alphaTag":"Newport PD 8","tgGroup":"Newport","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":168,"systemId":197,"tgid":1303,"name":"Fire Dispatch/Operations","alphaTag":"Newport FD1","tgGroup":"Newport","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":169,"systemId":197,"tgid":1305,"name":"Fireground Ops 1","alphaTag":"Newport FG1","tgGroup":"Newport","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":170,"systemId":197,"tgid":1306,"name":"Fireground Ops 2","alphaTag":"Newport FG2","tgGroup":"Newport","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":171,"systemId":197,"tgid":1301,"name":"Fire - Training","alphaTag":"Newport FDT","tgGroup":"Newport","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":172,"systemId":197,"tgid":1291,"name":"Water Department","alphaTag":"Newport Water","tgGroup":"Newport","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":173,"systemId":197,"tgid":1293,"name":"Public Works","alphaTag":"Newport DPW","tgGroup":"Newport","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":174,"systemId":197,"tgid":1297,"name":"Citywide Events","alphaTag":"Newport Evnt","tgGroup":"Newport","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":175,"systemId":197,"tgid":1312,"name":"Newport Citywide","alphaTag":"Newport CW","tgGroup":"Newport","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":176,"systemId":197,"tgid":1285,"name":"Police 1 - Dispatch","alphaTag":"NKing PD 1","tgGroup":"North Kingstown","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":177,"systemId":197,"tgid":1286,"name":"Police 2 - Admin","alphaTag":"NKing PD 2","tgGroup":"North Kingstown","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":178,"systemId":197,"tgid":1287,"name":"Police 3 - Car/Car","alphaTag":"NKing PD 3","tgGroup":"North Kingstown","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":179,"systemId":197,"tgid":1280,"name":"Fire - Dispatch","alphaTag":"NKing Fire D","tgGroup":"North Kingstown","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":180,"systemId":197,"tgid":1281,"name":"Fire - Fireground","alphaTag":"NKing Fire G","tgGroup":"North Kingstown","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":181,"systemId":197,"tgid":1536,"name":"Police 1 - Dispatch","alphaTag":"NorthPrv PD1","tgGroup":"North Providence","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":182,"systemId":197,"tgid":1537,"name":"Police 2 - Car/Car","alphaTag":"NorthPrv PD2","tgGroup":"North Providence","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":183,"systemId":197,"tgid":1538,"name":"Police 3 - Tactical","alphaTag":"NorthPrv PD3","tgGroup":"North Providence","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":184,"systemId":197,"tgid":1547,"name":"Fire Dispatch ","alphaTag":"NorthPrv FDD","tgGroup":"North Providence","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":185,"systemId":197,"tgid":1548,"name":"Fire 2","alphaTag":"NorthPrv Fire 2","tgGroup":"North Providence","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":186,"systemId":197,"tgid":1549,"name":"Fire 3","alphaTag":"NorthPrv Fire 3","tgGroup":"North Providence","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":187,"systemId":197,"tgid":1550,"name":"Fire 4","alphaTag":"NorthPrv Fire 4","tgGroup":"North Providence","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":188,"systemId":197,"tgid":1551,"name":"Fire 5","alphaTag":"NorthPrv Fire 5","tgGroup":"North Providence","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":189,"systemId":197,"tgid":1552,"name":"Fire 6","alphaTag":"NorthPrv Fire 6","tgGroup":"North Providence","metadata":{"encrypted":true},"tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":190,"systemId":197,"tgid":1544,"name":"Townwide 1","alphaTag":"NorthPrv TownW 1","tgGroup":"North Providence","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":191,"systemId":197,"tgid":1545,"name":"Townwide 2","alphaTag":"NorthPrv TownW 2","tgGroup":"North Providence","tags":["Interop"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":192,"systemId":197,"tgid":1554,"name":"Public Works","alphaTag":"NorthPrv DPW","tgGroup":"North Providence","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":193,"systemId":197,"tgid":1971,"name":"Police","alphaTag":"N Smithfd PD","tgGroup":"North Smithfield","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":194,"systemId":197,"tgid":1968,"name":"Fire Dispatch/Operations","alphaTag":"N Smithfield FD","tgGroup":"North Smithfield","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":195,"systemId":197,"tgid":1969,"name":"Fire Secondary","alphaTag":"N Smithfield FD2","tgGroup":"North Smithfield","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":196,"systemId":197,"tgid":1981,"name":"Fireground","alphaTag":"N Smithfield FD3","tgGroup":"North Smithfield","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":197,"systemId":197,"tgid":1440,"name":"Fire - Operations","alphaTag":"Pawtucket FD 1","tgGroup":"Pawtucket","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":198,"systemId":197,"tgid":1441,"name":"Fireground","alphaTag":"Pawtucket FG","tgGroup":"Pawtucket","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":199,"systemId":197,"tgid":1442,"name":"EMS Tac","alphaTag":"Pawtucket EMSTac","tgGroup":"Pawtucket","tags":["EMS-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":200,"systemId":197,"tgid":1248,"name":"Police","alphaTag":"PortsmouthPD","tgGroup":"Portsmouth","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":201,"systemId":197,"tgid":1253,"name":"Fire Dispatch (Patch to VHF Primary)","alphaTag":"Portsmouth FD","tgGroup":"Portsmouth","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":202,"systemId":197,"tgid":1255,"name":"Fireground","alphaTag":"Portsmouth FG","tgGroup":"Portsmouth","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":203,"systemId":197,"tgid":1262,"name":"Island Fire Dispatch","alphaTag":"Prudence Isl FD","tgGroup":"Portsmouth","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":204,"systemId":197,"tgid":10000,"name":"Police - All Call - Emergency Broadcasts","alphaTag":"PPD ATG","tgGroup":"Providence (City)","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":205,"systemId":197,"tgid":10001,"name":"Police 1 - Dispatch","alphaTag":"PPD CH 1","tgGroup":"Providence (City)","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":206,"systemId":197,"tgid":10002,"name":"Police 2","alphaTag":"PPD CH 2","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":207,"systemId":197,"tgid":10003,"name":"Police 3","alphaTag":"PPD CH 3","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":208,"systemId":197,"tgid":10004,"name":"Police 4","alphaTag":"PPD CH-4","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":209,"systemId":197,"tgid":10005,"name":"Police 5 -Detectives 1","alphaTag":"PPD DETEC 1","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":210,"systemId":197,"tgid":10006,"name":"Police 6 - Car-to-Car","alphaTag":"PPD T/A","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":211,"systemId":197,"tgid":10007,"name":"Police 7 - Narcotics 1","alphaTag":"PPD NARC 1","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":212,"systemId":197,"tgid":10008,"name":"Police 8 - Narcotics 2","alphaTag":"PPD NARC 2","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":213,"systemId":197,"tgid":10009,"name":"Police 9 - Detectives 2","alphaTag":"PPD DETEC 2","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":214,"systemId":197,"tgid":10010,"name":"Police 10 - Special Details 1","alphaTag":"PPD DETAIL 1","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":215,"systemId":197,"tgid":10011,"name":"Police 11 - Special Details 2","alphaTag":"PPD DETAIL 2","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":216,"systemId":197,"tgid":10012,"name":"Police 12 - Corrections Security","alphaTag":"PPD CORR SEC","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":217,"systemId":197,"tgid":10013,"name":"Police 13 - Special Response Unit","alphaTag":"PPD SRU","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":218,"systemId":197,"tgid":10014,"name":"Police 14 - Administration","alphaTag":"PPD ADMIN","tgGroup":"Providence (City)","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":219,"systemId":197,"tgid":10100,"name":"Fire All Call - Emergency Broadcasts","alphaTag":"PROV FD ATG","tgGroup":"Providence (City)","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":220,"systemId":197,"tgid":10101,"name":"Fire Dispatch","alphaTag":"PFD DISPATCH","tgGroup":"Providence (City)","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":221,"systemId":197,"tgid":10107,"name":"Fireground 2","alphaTag":"PFD CH-2 FG","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":222,"systemId":197,"tgid":10108,"name":"Fireground 3","alphaTag":"PFD CH-3 FG","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":223,"systemId":197,"tgid":10109,"name":"Fireground 4","alphaTag":"PFD CH-4 FG","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":224,"systemId":197,"tgid":10102,"name":"Fire 5","alphaTag":"PFD CH-5","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":225,"systemId":197,"tgid":10103,"name":"Fire 6","alphaTag":"PFD CH-6","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":226,"systemId":197,"tgid":10104,"name":"Fire 7","alphaTag":"PFD CH-7","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":227,"systemId":197,"tgid":10110,"name":"Fire - Mutual Aid 1","alphaTag":"PFD M/A 1","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":228,"systemId":197,"tgid":10111,"name":"Fire - Mutual Aid 2","alphaTag":"PFD M/A 2","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":229,"systemId":197,"tgid":10112,"name":"Fire - Mutual Aid 3","alphaTag":"PFD M/A 3","tgGroup":"Providence (City)","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":230,"systemId":197,"tgid":10113,"name":"Fireground 8","alphaTag":"PFD Fireground 8","tgGroup":"Providence (City)","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":231,"systemId":197,"tgid":10105,"name":"Fire - Administration","alphaTag":"PFD ADMIN","tgGroup":"Providence (City)","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":232,"systemId":197,"tgid":10106,"name":"Fire - Communications","alphaTag":"PFD COMM","tgGroup":"Providence (City)","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":233,"systemId":197,"tgid":10207,"name":"Public Works","alphaTag":"PROV DPW","tgGroup":"Providence (City)","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":234,"systemId":197,"tgid":2035,"name":"Police","alphaTag":"Richmond PD","tgGroup":"Richmond","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":235,"systemId":197,"tgid":2042,"name":"Chariho Regional High School","alphaTag":"Chariho Reg HS","tgGroup":"Richmond","tags":["Schools"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":236,"systemId":197,"tgid":1460,"name":"Police","alphaTag":"Scituate PD","tgGroup":"Scituate","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":237,"systemId":197,"tgid":1463,"name":"Fire Operations","alphaTag":"Scituate FD","tgGroup":"Scituate","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":238,"systemId":197,"tgid":1651,"name":"Police Operations","alphaTag":"SmithfieldPD","tgGroup":"Smithfield","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":239,"systemId":197,"tgid":1652,"name":"Police Secondary","alphaTag":"Smfld PD 2","tgGroup":"Smithfield","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":240,"systemId":197,"tgid":1653,"name":"Police Detectives","alphaTag":"Smfld PD Det","tgGroup":"Smithfield","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":241,"systemId":197,"tgid":1654,"name":"Police Admin","alphaTag":"Smfld PD Adm","tgGroup":"Smithfield","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":242,"systemId":197,"tgid":1661,"name":"Police Details","alphaTag":"Smfld PD Dtl","tgGroup":"Smithfield","tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":243,"systemId":197,"tgid":1648,"name":"Fire - Fireground","alphaTag":"SmithfieldFD","tgGroup":"Smithfield","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":244,"systemId":197,"tgid":1655,"name":"Town-Wide","alphaTag":"Smfld Town","tgGroup":"Smithfield","tags":["Multi-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":245,"systemId":197,"tgid":1657,"name":"Emergency Management","alphaTag":"Smfld EMA","tgGroup":"Smithfield","tags":["Emergency Ops"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":246,"systemId":197,"tgid":1660,"name":"Public Works","alphaTag":"Smfld DPW","tgGroup":"Smithfield","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":247,"systemId":197,"tgid":1225,"name":"Police 1 - Dispatch","alphaTag":"SKing PD 1","tgGroup":"South Kingstown","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":248,"systemId":197,"tgid":1226,"name":"Police 2 - Car/Car","alphaTag":"SKing PD 2","tgGroup":"South Kingstown","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":249,"systemId":197,"tgid":1235,"name":"Police 3 - Tactical","alphaTag":"SKing PD 3","tgGroup":"South Kingstown","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":250,"systemId":197,"tgid":1236,"name":"Police 5 - Tactical","alphaTag":"SKing PD 5","tgGroup":"South Kingstown","metadata":{"encrypted":true},"tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":251,"systemId":197,"tgid":1232,"name":"Fire - UHF Simulcast","alphaTag":"SKing FD Lnk","tgGroup":"South Kingstown","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":252,"systemId":197,"tgid":1240,"name":"Fire - Detail","alphaTag":"SKing Fire D","tgGroup":"South Kingstown","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":253,"systemId":197,"tgid":1227,"name":"Union Fire District - Fireground 1","alphaTag":"UnionFD FG 1","tgGroup":"South Kingstown","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":254,"systemId":197,"tgid":1237,"name":"Union Fire District - Fireground 2","alphaTag":"UnionFD FG 2","tgGroup":"South Kingstown","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":255,"systemId":197,"tgid":1026,"name":"Union Fire District - Special Events","alphaTag":"UnionFD Evnt","tgGroup":"South Kingstown","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":256,"systemId":197,"tgid":1015,"name":"EMS","alphaTag":"SKing EMS","tgGroup":"South Kingstown","metadata":{"encrypted":true},"tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":257,"systemId":197,"tgid":1316,"name":"Police (Simulcast 482.9625)","alphaTag":"Tiverton PD","tgGroup":"Tiverton","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":258,"systemId":197,"tgid":1315,"name":"Fire (Simulcast 471.7875)","alphaTag":"Tiverton FD","tgGroup":"Tiverton","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":259,"systemId":197,"tgid":1162,"name":"Fire","alphaTag":"Warwick FD","tgGroup":"Warwick","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":260,"systemId":197,"tgid":1170,"name":"Fireground","alphaTag":"Warwick FG","tgGroup":"Warwick","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":261,"systemId":197,"tgid":1805,"name":"Police","alphaTag":"W Greenwh PD","tgGroup":"West Greenwich","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":262,"systemId":197,"tgid":1806,"name":"Police Secondary","alphaTag":"W GreenwichPD2","tgGroup":"West Greenwich","tags":["Law Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":263,"systemId":197,"tgid":1208,"name":"Fire Operations","alphaTag":"W Warwick FD","tgGroup":"West Warwick","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":264,"systemId":197,"tgid":1050,"name":"Police 1 - Dispatch","alphaTag":"Westerly PD1","tgGroup":"Westerly","metadata":{"encrypted":true},"tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":265,"systemId":197,"tgid":1051,"name":"Police 2","alphaTag":"Westerly PD2","tgGroup":"Westerly","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":266,"systemId":197,"tgid":1052,"name":"Police 3","alphaTag":"Westerly PD3","tgGroup":"Westerly","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":267,"systemId":197,"tgid":1053,"name":"Police 4","alphaTag":"Westerly PD4","tgGroup":"Westerly","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":268,"systemId":197,"tgid":1054,"name":"Police 5 - Reserve Officers","alphaTag":"Westerly PD5","tgGroup":"Westerly","tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":269,"systemId":197,"tgid":1064,"name":"Police 6 - Traffic Division","alphaTag":"Westerly PD6","tgGroup":"Westerly","tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":270,"systemId":197,"tgid":1063,"name":"Fire Operations","alphaTag":"Westerly FD","tgGroup":"Westerly","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":271,"systemId":197,"tgid":1072,"name":"Police/Fire/EMS Ops","alphaTag":"Westerly PFE","tgGroup":"Westerly","tags":["Multi-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":272,"systemId":197,"tgid":1082,"name":"EMS Operations","alphaTag":"Westerly EMS ","tgGroup":"Westerly","tags":["EMS Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":273,"systemId":197,"tgid":1363,"name":"Police 1 - Dispatch","alphaTag":"Woonskt PD 1","tgGroup":"Woonsocket","tags":["Law Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":274,"systemId":197,"tgid":1364,"name":"Police 2","alphaTag":"Woonskt PD 2","tgGroup":"Woonsocket","metadata":{"encrypted":true},"tags":["Law Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":275,"systemId":197,"tgid":1360,"name":"Fire Dispatch - Operations","alphaTag":"Woonsocket FD D","tgGroup":"Woonsocket","tags":["Fire-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":276,"systemId":197,"tgid":1361,"name":"Fire Secondary","alphaTag":"Woonsocket FD 2","tgGroup":"Woonsocket","tags":["Fire Dispatch"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":277,"systemId":197,"tgid":1354,"name":"Fire - Fireground 3","alphaTag":"Woonskt FD 3","tgGroup":"Woonsocket","tags":["Fire-Tac"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":278,"systemId":197,"tgid":1367,"name":"Citywide","alphaTag":"Woonskt City","tgGroup":"Woonsocket","tags":["Multi-Talk"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":279,"systemId":197,"tgid":1368,"name":"Public Works - Streets","alphaTag":"Woonsocket PW","tgGroup":"Woonsocket","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":280,"systemId":197,"tgid":1,"name":"RISCON Radio Technicians","alphaTag":"Radio Techs","tgGroup":"Radio Technicians","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false},{"id":281,"systemId":197,"tgid":10125,"name":"RISCON Radio Technicians","alphaTag":"Radio Techs","tgGroup":"Radio Technicians","tags":["Public Works"],"alert":true,"weight":1,"system":{"id":197,"name":"RISCON"},"learned":false}] \ No newline at end of file diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 54898f7..cfa8b86 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -11,6 +11,7 @@ sql: query_parameter_limit: 3 emit_json_tags: true emit_interface: true + json_tags_case_style: camel initialisms: - id - tgid -- 2.48.1 From e5772ae2cee75a281577977d7b8dbcf38133a472 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 7 Feb 2025 09:45:03 -0500 Subject: [PATCH 42/58] camcelcase client --- client/stillbox/src/app/calls.ts | 4 +-- .../calls/call-info/call-info.component.html | 2 +- .../src/app/calls/calls.component.html | 6 ++--- .../stillbox/src/app/calls/calls.service.ts | 8 +++--- .../incident/incident.component.html | 6 ++--- client/stillbox/src/app/talkgroup.ts | 26 +++++++++---------- .../talkgroups/import/import.component.html | 4 +-- .../talkgroup-record.component.html | 10 +++---- .../talkgroup-record.component.ts | 18 ++++++------- .../talkgroup-table.component.html | 4 +-- .../src/app/talkgroups/talkgroups.service.ts | 6 ++--- 11 files changed, 47 insertions(+), 47 deletions(-) diff --git a/client/stillbox/src/app/calls.ts b/client/stillbox/src/app/calls.ts index dddf93f..ed581f5 100644 --- a/client/stillbox/src/app/calls.ts +++ b/client/stillbox/src/app/calls.ts @@ -1,9 +1,9 @@ export interface CallRecord { id: string; - call_date: Date; + callDate: Date; audioURL: string | null; duration: number; - system_id: number; + systemId: number; tgid: number; incidents: number; // in incident } diff --git a/client/stillbox/src/app/calls/call-info/call-info.component.html b/client/stillbox/src/app/calls/call-info/call-info.component.html index f15bf1a..aa51826 100644 --- a/client/stillbox/src/app/calls/call-info/call-info.component.html +++ b/client/stillbox/src/app/calls/call-info/call-info.component.html @@ -2,7 +2,7 @@

{{ call | talkgroup: "alpha" | async }} @ - {{ call.call_date | grabDate }} {{ call.call_date | time: true }} + {{ call.callDate | grabDate }} {{ call.callDate | time: true }}

diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index 7c2891d..3a3972f 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -135,13 +135,13 @@ Date - {{ call.call_date | grabDate }} + {{ call.callDate | grabDate }} Time - - {{ call.call_date | time }} + + {{ call.callDate | time }} diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index 1b433e8..31816ff 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -57,19 +57,19 @@ export class TalkgroupPipe implements PipeTransform { field: string, share: Share | null = null, ): Observable { - return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe( + return this.tgService.getTalkgroup(call.systemId, call.tgid).pipe( map((tg: Talkgroup) => { switch (field) { case 'alpha': { - return tg.alpha_tag ?? call.tgid; + return tg.alphaTag ?? call.tgid; break; } case 'group': { - return tg.tg_group ?? '\u2014'; + return tg.tgGroup ?? '\u2014'; break; } case 'system': { - return tg.system?.name ?? tg.system_id.toString(); + return tg.system?.name ?? tg.systemId.toString(); } default: { return tg.name ?? '\u2014'; diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 8a8b159..3e18495 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -75,13 +75,13 @@ Date - {{ call.call_date | grabDate }} + {{ call.callDate | grabDate }} Time - - {{ call.call_date | time }} + + {{ call.callDate | time }} diff --git a/client/stillbox/src/app/talkgroup.ts b/client/stillbox/src/app/talkgroup.ts index 02ba326..d1e6a79 100644 --- a/client/stillbox/src/app/talkgroup.ts +++ b/client/stillbox/src/app/talkgroup.ts @@ -58,33 +58,33 @@ export const iconMapping: IconMap = { export class Talkgroup { id!: number; - system_id!: number; + systemId!: number; tgid!: number; name!: string; - alpha_tag!: string; - tg_group!: string; + alphaTag!: string; + tgGroup!: string; frequency!: number; metadata!: Metadata | null; tags!: string[]; alert!: boolean; system?: System; - alert_rules!: AlertRule[]; + alertRules!: AlertRule[]; weight!: number; learned?: boolean; icon?: string; iconSvg?: string; constructor( id: number, - system_id: number, + systemId: number, tgid: number, name: string, - alpha_tag: string, - tg_group: string, + alphaTag: string, + tgGroup: string, frequency: number, metadata: Metadata | null, tags: string[], alert: boolean, - alert_rules: AlertRule[], + alertRules: AlertRule[], weight: number, system?: System, learned?: boolean, @@ -99,7 +99,7 @@ export class Talkgroup { tgTuple(): TGID { return { - sys: this.system_id, + sys: this.systemId, tg: this.tgid, }; } @@ -111,15 +111,15 @@ export interface TalkgroupUI extends Talkgroup { export interface TalkgroupUpdate { id: number; - system_id: number; + systemId: number; tgid: number; name: string | null; - alpha_tag: string | null; - tg_group: string | null; + alphaTag: string | null; + tgGroup: string | null; frequency: number | null; metadata: Object | null; tags: string[] | null; alert: boolean | null; - alert_rules: AlertRule[] | null; + alertRules: AlertRule[] | null; weight: number | null; } diff --git a/client/stillbox/src/app/talkgroups/import/import.component.html b/client/stillbox/src/app/talkgroups/import/import.component.html index bc3c8d1..4aec0f5 100644 --- a/client/stillbox/src/app/talkgroups/import/import.component.html +++ b/client/stillbox/src/app/talkgroups/import/import.component.html @@ -55,8 +55,8 @@ {{ tg.system?.name }} {{ tg.system?.id }} - {{ tg.tg_group }} - {{ tg.alpha_tag }} + {{ tg.tgGroup }} + {{ tg.alphaTag }} {{ tg.name }} {{ tg.tgid }} {{ tg?.metadata?.encrypted ? "E" : "" }} diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html index 9e8a867..25ead7a 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html @@ -15,9 +15,9 @@ Alpha Tag
@@ -26,9 +26,9 @@ Group
@@ -108,7 +108,7 @@ >
- +
} @else { diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index 75efd33..967fc50 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -84,8 +84,8 @@ export class TalkgroupRecordComponent { readonly _allTags: Observable; form = new FormGroup({ name: new FormControl(''), - alpha_tag: new FormControl(''), - tg_group: new FormControl(''), + alphaTag: new FormControl(''), + tgGroup: new FormControl(''), frequency: new FormControl(0), alert: new FormControl(false), weight: new FormControl(0.0), @@ -158,8 +158,8 @@ export class TalkgroupRecordComponent { .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) .pipe( tap((tg) => { - tg.alert_rules = tg.alert_rules - ? tg.alert_rules.map((x) => Object.assign(new AlertRule(), x)) + tg.alertRules = tg.alertRules + ? tg.alertRules.map((x) => Object.assign(new AlertRule(), x)) : []; this.form.patchValue(tg); this.form.controls['tagInput'].setValue(''); @@ -180,17 +180,17 @@ export class TalkgroupRecordComponent { save() { let tgu: TalkgroupUpdate = { - system_id: this.tgid.sys, + systemId: this.tgid.sys, tgid: this.tgid.tg, }; if (this.form.controls['name'].dirty) { tgu.name = this.form.controls['name'].value; } - if (this.form.controls['alpha_tag'].dirty) { - tgu.alpha_tag = this.form.controls['alpha_tag'].value; + if (this.form.controls['alphaTag'].dirty) { + tgu.alphaTag = this.form.controls['alphaTag'].value; } - if (this.form.controls['tg_group'].dirty) { - tgu.tg_group = this.form.controls['tg_group'].value; + if (this.form.controls['tgGroup'].dirty) { + tgu.tgGroup = this.form.controls['tgGroup'].value; } if (this.form.controls['frequency'].dirty) { tgu.frequency = this.form.controls['frequency'].value; diff --git a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html index 780f1b3..31a8635 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html @@ -34,7 +34,7 @@
Group - {{ tg.tg_group }} + {{ tg.tgGroup }} Name @@ -42,7 +42,7 @@ Alpha Tag - {{ tg.alpha_tag }} + {{ tg.alphaTag }} TG ID diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 7fc6026..1184952 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -90,10 +90,10 @@ export class TalkgroupService { } putTalkgroup(tu: TalkgroupUpdate): Observable { - let tgid = this.tgKey(tu.system_id, tu.tgid); + let tgid = this.tgKey(tu.systemId, tu.tgid); return this.http - .put(`/api/talkgroup/${tu.system_id}/${tu.tgid}`, tu) + .put(`/api/talkgroup/${tu.systemId}/${tu.tgid}`, tu) .pipe( switchMap((tg) => { let tObs = this._getTalkgroup.get(tgid); @@ -128,7 +128,7 @@ export class TalkgroupService { this.subscriptions.add( this.tgs$.subscribe((tgs) => { tgs.forEach((tg) => { - let tgid = this.tgKey(tg.system_id, tg.tgid); + let tgid = this.tgKey(tg.systemId, tg.tgid); const rs = this._getTalkgroup.get(tgid); if (rs) { (rs as ReplaySubject).next(tg); -- 2.48.1 From 9b55805acdff6e0576956158f9836bb6f7f3810c Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 7 Feb 2025 17:01:17 -0500 Subject: [PATCH 43/58] Sort tgs --- pkg/rest/talkgroups.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index 8e251d8..5246109 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "slices" "dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/pkg/database" @@ -181,6 +182,14 @@ func (tga *talkgroupAPI) getTGsShareRoute(_ ID, w http.ResponseWriter, r *http.R idSl = append(idSl, id) } + slices.SortFunc(idSl, func(a, b talkgroups.ID) int { + if d := int(a.System) - int(b.System); d != 0 { + return d + } + + return int(a.Talkgroup) - int(b.Talkgroup) + }) + tgRes, err := tgs.TGs(ctx, idSl) if err != nil { wErr(w, r, autoError(err)) -- 2.48.1 From bf413c2e099a130ece536bb71e44255842733644 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 9 Feb 2025 09:29:08 -0500 Subject: [PATCH 44/58] wip --- client/stillbox/src/app/app.component.html | 4 +- client/stillbox/src/app/app.config.ts | 2 +- client/stillbox/src/app/login/auth.service.ts | 107 ++++++++++++------ .../src/app/login/login.component.html | 2 +- .../stillbox/src/app/login/login.component.ts | 28 +---- .../src/app/talkgroups/talkgroups.service.ts | 2 +- 6 files changed, 82 insertions(+), 63 deletions(-) diff --git a/client/stillbox/src/app/app.component.html b/client/stillbox/src/app/app.component.html index 20725bc..3735317 100644 --- a/client/stillbox/src/app/app.component.html +++ b/client/stillbox/src/app/app.component.html @@ -8,7 +8,7 @@
- @if (auth.loggedIn) { + @if (auth.isAuth()) { @@ -19,7 +19,7 @@
- @if (auth.loggedIn) { + @if (auth.isAuth()) { } @else {
diff --git a/client/stillbox/src/app/app.config.ts b/client/stillbox/src/app/app.config.ts index 0014fed..cb21be6 100644 --- a/client/stillbox/src/app/app.config.ts +++ b/client/stillbox/src/app/app.config.ts @@ -29,7 +29,7 @@ export function authIntercept( next: HttpHandlerFn, ): Observable> { let authSvc: AuthService = inject(AuthService); - if (authSvc.loggedIn) { + if (authSvc.isAuth()) { req = req.clone({ setHeaders: { Authorization: `Bearer ${authSvc.getToken()}`, diff --git a/client/stillbox/src/app/login/auth.service.ts b/client/stillbox/src/app/login/auth.service.ts index 0ec7b97..8749535 100644 --- a/client/stillbox/src/app/login/auth.service.ts +++ b/client/stillbox/src/app/login/auth.service.ts @@ -1,56 +1,93 @@ -import { Injectable } from '@angular/core'; +import { Injectable, signal, computed, effect, inject, DestroyRef } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Router } from '@angular/router'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class Jwt { constructor(public jwt: string) {} } +type AuthState = { + user: string | null; + token: string | null; + is_auth: boolean; +}; + @Injectable({ providedIn: 'root', }) export class AuthService { - loggedIn: boolean = false; + private _accessTokenKey = 'jwt'; + private _storedToken = localStorage.getItem(this._accessTokenKey); + destroyed = inject(DestroyRef); + + private _state = signal({ + user: null, + token: this._storedToken, + is_auth: this._storedToken !== null, + }); + loginFailed = signal(false); + token = computed(() => this._state().token); + isAuth = computed(() => this._state().is_auth); + user = computed(() => this._state().user); constructor( private http: HttpClient, private _router: Router, ) { - let ssJWT = localStorage.getItem('jwt'); - if (ssJWT) { - this.loggedIn = true; - } + effect(() => { + const token = this.token(); + if (token !== null) { + localStorage.setItem(this._accessTokenKey, token); + } else { + localStorage.removeItem(this._accessTokenKey); + } + }); } - login(username: string, password: string): Observable> { + login(username: string, password: string) { return this.http - .post( - '/api/login', - { username: username, password: password }, - { observe: 'response' }, - ) - .pipe( - tap((event) => { - if (event.status == 200) { - localStorage.setItem('jwt', event.body?.jwt.toString() ?? ''); - this.loggedIn = true; - this._router.navigateByUrl('/home'); - } - }), - ); + .post('/api/login', { username: username, password: password }) + .pipe(takeUntilDestroyed(this.destroyed)) + .subscribe({ + next: (res) => { + let state = { + user: username, + token: res.jwt, + is_auth: true, + }; + this._state.set(state); + this.loginFailed.update(() => false); + this._router.navigateByUrl('/'); + }, + error: (err) => { + this.loginFailed.update(() => true); + }, + }); + } + + _clearState() { +this._state.update((state) => { + state.is_auth = false; + state.token = null; + return state; + }); + } logout() { + console.log("logout!"); this.http - .get('/api/logout', { withCredentials: true, observe: 'response' }) - .subscribe((event) => { - if (event.status == 200) { - this.loggedIn = false; - } - }); - localStorage.removeItem('jwt'); - this.loggedIn = false; + .get('/api/logout', { withCredentials: true}) + .subscribe({ + next: (event) => { + this._clearState(); + }, + error: (err) => { + this._clearState(); + } + }); this._router.navigateByUrl('/login'); } @@ -60,14 +97,18 @@ export class AuthService { .pipe( tap((event) => { if (event.status == 200) { - localStorage.setItem('jwt', event.body?.jwt.toString() ?? ''); - this.loggedIn = true; + let tok = event.body?.jwt.toString(); + this._state.update((state) => { + state.is_auth = true; + state.token = tok ? tok : null; + return state; + }); } }), ); } getToken(): string | null { - return localStorage.getItem('jwt'); + return localStorage.getItem(this._accessTokenKey); } } diff --git a/client/stillbox/src/app/login/login.component.html b/client/stillbox/src/app/login/login.component.html index 293daed..1646e47 100644 --- a/client/stillbox/src/app/login/login.component.html +++ b/client/stillbox/src/app/login/login.component.html @@ -28,7 +28,7 @@
- @if (failed) { + @if (failed()) {
Login Failed!
diff --git a/client/stillbox/src/app/login/login.component.ts b/client/stillbox/src/app/login/login.component.ts index 5a8c5bc..a7adc76 100644 --- a/client/stillbox/src/app/login/login.component.ts +++ b/client/stillbox/src/app/login/login.component.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AuthService } from '../login/auth.service'; import { catchError, of, Subscription } from 'rxjs'; @@ -17,31 +17,9 @@ export class LoginComponent { router: Router = inject(Router); username: string = ''; password: string = ''; - failed: boolean = false; - private subscriptions = new Subscription(); + failed = this.apiService.loginFailed; onSubmit() { - this.failed = false; - this.subscriptions.add( - this.apiService - .login(this.username, this.password) - .pipe( - catchError(() => { - this.failed = true; - return of(null); - }), - ) - .subscribe((event) => { - if (event?.status == 200) { - this.router.navigateByUrl('/'); - } else { - this.failed = true; - } - }), - ); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); + this.apiService.login(this.username, this.password); } } diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 1184952..d132b23 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -51,7 +51,7 @@ export class TalkgroupService { if (sh) { this.shareSvc.getShare(sh).subscribe(this.fetchAll); } else { - if (this.authSvc.loggedIn) { + if (this.authSvc.isAuth()) { this.fetchAll.next(null); } } -- 2.48.1 From 88522f4e241393ebcd152fd201c49a159b8b8340 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 9 Feb 2025 09:34:43 -0500 Subject: [PATCH 45/58] Immutable signals --- client/stillbox/src/app/login/auth.service.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/client/stillbox/src/app/login/auth.service.ts b/client/stillbox/src/app/login/auth.service.ts index 8749535..46fc5df 100644 --- a/client/stillbox/src/app/login/auth.service.ts +++ b/client/stillbox/src/app/login/auth.service.ts @@ -1,4 +1,11 @@ -import { Injectable, signal, computed, effect, inject, DestroyRef } from '@angular/core'; +import { + Injectable, + signal, + computed, + effect, + inject, + DestroyRef, +} from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Router } from '@angular/router'; import { Observable, Subject } from 'rxjs'; @@ -52,11 +59,11 @@ export class AuthService { .pipe(takeUntilDestroyed(this.destroyed)) .subscribe({ next: (res) => { - let state = { - user: username, - token: res.jwt, - is_auth: true, - }; + let state = { + user: username, + token: res.jwt, + is_auth: true, + }; this._state.set(state); this.loginFailed.update(() => false); this._router.navigateByUrl('/'); @@ -68,25 +75,17 @@ export class AuthService { } _clearState() { -this._state.update((state) => { - state.is_auth = false; - state.token = null; - return state; - }); - + this._state.set({}); } logout() { - console.log("logout!"); - this.http - .get('/api/logout', { withCredentials: true}) - .subscribe({ - next: (event) => { - this._clearState(); - }, + this.http.get('/api/logout', { withCredentials: true }).subscribe({ + next: (event) => { + this._clearState(); + }, error: (err) => { - this._clearState(); - } + this._clearState(); + }, }); this._router.navigateByUrl('/login'); } @@ -97,12 +96,14 @@ this._state.update((state) => { .pipe( tap((event) => { if (event.status == 200) { + let ost = this._state(); let tok = event.body?.jwt.toString(); - this._state.update((state) => { - state.is_auth = true; - state.token = tok ? tok : null; - return state; - }); + let state = { + user: ost.user, + token: tok ? tok : null, + is_auth: true, + }; + this._state.set(state); } }), ); -- 2.48.1 From 246c024fc2b165ccb4575462c18042806a4d9b2d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 9 Feb 2025 09:49:18 -0500 Subject: [PATCH 46/58] Fix #107 --- client/stillbox/src/app/talkgroups/talkgroups.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index d132b23..fee62a3 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -59,7 +59,9 @@ export class TalkgroupService { } setShare(share: Share | null) { - this.fetchAll.next(share); + if (!this.authSvc.isAuth() && share !== null) { + this.fetchAll.next(share); + } } ngOnDestroy() { -- 2.48.1 From 3a4c90d12fff47e09cd20f359f82764391d50747 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 9 Feb 2025 10:03:52 -0500 Subject: [PATCH 47/58] Fix authGuard --- client/stillbox/src/app/auth.guard.ts | 2 +- client/stillbox/src/app/login/auth.service.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/stillbox/src/app/auth.guard.ts b/client/stillbox/src/app/auth.guard.ts index b9b91c0..d2383ce 100644 --- a/client/stillbox/src/app/auth.guard.ts +++ b/client/stillbox/src/app/auth.guard.ts @@ -5,7 +5,7 @@ import { inject } from '@angular/core'; export const AuthGuard: CanActivateFn = (route, state) => { const router: Router = inject(Router); const authSvc: AuthService = inject(AuthService); - if (localStorage.getItem('jwt') == null) { + if (authSvc.token() === null) { let success = false; authSvc.refresh().subscribe({ next: (event) => { diff --git a/client/stillbox/src/app/login/auth.service.ts b/client/stillbox/src/app/login/auth.service.ts index 46fc5df..474c3ff 100644 --- a/client/stillbox/src/app/login/auth.service.ts +++ b/client/stillbox/src/app/login/auth.service.ts @@ -75,7 +75,10 @@ export class AuthService { } _clearState() { - this._state.set({}); + this._state.set({ + is_auth: false, + token: null, + }); } logout() { -- 2.48.1 From 04977b5468e9c062f53888195cf0dc7b82b7403d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 9 Feb 2025 12:37:27 -0500 Subject: [PATCH 48/58] Add incident created_at --- pkg/database/incidents.sql.go | 13 +++++++++++-- pkg/database/models.go | 1 + pkg/incidents/incident.go | 1 + pkg/incidents/incstore/store.go | 2 ++ pkg/pb/stillbox.pb.go | 2 +- sql/postgres/migrations/001_initial.up.sql | 1 + sql/postgres/queries/incidents.sql | 4 ++++ 7 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/database/incidents.sql.go b/pkg/database/incidents.sql.go index a894f1e..6e1491f 100644 --- a/pkg/database/incidents.sql.go +++ b/pkg/database/incidents.sql.go @@ -61,6 +61,7 @@ INSERT INTO incidents ( name, owner, description, + created_at, start_time, end_time, location, @@ -70,12 +71,13 @@ INSERT INTO incidents ( $2, $3, $4, + NOW(), $5, $6, $7, $8 ) -RETURNING id, name, owner, description, start_time, end_time, location, metadata +RETURNING id, name, owner, description, created_at, start_time, end_time, location, metadata ` type CreateIncidentParams struct { @@ -106,6 +108,7 @@ func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams) &i.Name, &i.Owner, &i.Description, + &i.CreatedAt, &i.StartTime, &i.EndTime, &i.Location, @@ -129,6 +132,7 @@ SELECT i.name, i.owner, i.description, + i.created_at, i.start_time, i.end_time, i.location, @@ -145,6 +149,7 @@ func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, erro &i.Name, &i.Owner, &i.Description, + &i.CreatedAt, &i.StartTime, &i.EndTime, &i.Location, @@ -297,6 +302,7 @@ SELECT i.name, i.owner, i.description, + i.created_at, i.start_time, i.end_time, i.location, @@ -335,6 +341,7 @@ type ListIncidentsPRow struct { Name string `json:"name"` Owner int `json:"owner"` Description *string `json:"description"` + CreatedAt pgtype.Timestamptz `json:"createdAt"` StartTime pgtype.Timestamptz `json:"startTime"` EndTime pgtype.Timestamptz `json:"endTime"` Location []byte `json:"location"` @@ -363,6 +370,7 @@ func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) &i.Name, &i.Owner, &i.Description, + &i.CreatedAt, &i.StartTime, &i.EndTime, &i.Location, @@ -411,7 +419,7 @@ SET metadata = COALESCE($6, metadata) WHERE id = $7 -RETURNING id, name, owner, description, start_time, end_time, location, metadata +RETURNING id, name, owner, description, created_at, start_time, end_time, location, metadata ` type UpdateIncidentParams struct { @@ -440,6 +448,7 @@ func (q *Queries) UpdateIncident(ctx context.Context, arg UpdateIncidentParams) &i.Name, &i.Owner, &i.Description, + &i.CreatedAt, &i.StartTime, &i.EndTime, &i.Location, diff --git a/pkg/database/models.go b/pkg/database/models.go index 2f470b6..0d80a21 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -60,6 +60,7 @@ type Incident struct { Name string `json:"name,omitempty"` Owner int `json:"owner,omitempty"` Description *string `json:"description,omitempty"` + CreatedAt pgtype.Timestamptz `json:"createdAt,omitempty"` StartTime pgtype.Timestamptz `json:"startTime,omitempty"` EndTime pgtype.Timestamptz `json:"endTime,omitempty"` Location []byte `json:"location,omitempty"` diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index e8d2f7a..14ae1cb 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -19,6 +19,7 @@ type Incident struct { Owner users.UserID `json:"owner"` Name string `json:"name"` Description *string `json:"description,omitempty"` + CreatedAt jsontypes.Time `json:"createdAt"` StartTime *jsontypes.Time `json:"startTime,omitempty"` EndTime *jsontypes.Time `json:"endTime,omitempty"` Location jsontypes.Location `json:"location,omitempty"` diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 9113690..81ce14d 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -231,6 +231,7 @@ func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident { Owner: users.UserID(d.Owner), Name: d.Name, Description: d.Description, + CreatedAt: jsontypes.Time(d.CreatedAt.Time), StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime), EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime), Metadata: d.Metadata, @@ -250,6 +251,7 @@ func fromDBListInPRow(id uuid.UUID, d database.ListIncidentsPRow) Incident { Owner: users.UserID(d.Owner), Name: d.Name, Description: d.Description, + CreatedAt: jsontypes.Time(d.CreatedAt.Time), StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime), EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime), Metadata: d.Metadata, diff --git a/pkg/pb/stillbox.pb.go b/pkg/pb/stillbox.pb.go index f473933..2742bc8 100644 --- a/pkg/pb/stillbox.pb.go +++ b/pkg/pb/stillbox.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.33.0 -// protoc v5.28.3 +// protoc v5.29.3 // source: stillbox.proto package pb diff --git a/sql/postgres/migrations/001_initial.up.sql b/sql/postgres/migrations/001_initial.up.sql index 2a3995d..ea77bf7 100644 --- a/sql/postgres/migrations/001_initial.up.sql +++ b/sql/postgres/migrations/001_initial.up.sql @@ -143,6 +143,7 @@ CREATE TABLE IF NOT EXISTS incidents( name TEXT NOT NULL, owner INTEGER NOT NULL, description TEXT, + created_at TIMESTAMPTZ, start_time TIMESTAMPTZ, end_time TIMESTAMPTZ, location JSONB, diff --git a/sql/postgres/queries/incidents.sql b/sql/postgres/queries/incidents.sql index 0bc1e89..9dca9f6 100644 --- a/sql/postgres/queries/incidents.sql +++ b/sql/postgres/queries/incidents.sql @@ -42,6 +42,7 @@ INSERT INTO incidents ( name, owner, description, + created_at, start_time, end_time, location, @@ -51,6 +52,7 @@ INSERT INTO incidents ( @name, @owner, sqlc.narg('description'), + NOW(), sqlc.narg('start_time'), sqlc.narg('end_time'), sqlc.narg('location'), @@ -65,6 +67,7 @@ SELECT i.name, i.owner, i.description, + i.created_at, i.start_time, i.end_time, i.location, @@ -160,6 +163,7 @@ SELECT i.name, i.owner, i.description, + i.created_at, i.start_time, i.end_time, i.location, -- 2.48.1 From d16ad6a4ad13b260d06f3a1853966895d24f3d3d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 10 Feb 2025 21:29:32 -0500 Subject: [PATCH 49/58] Shares list endpoint --- pkg/database/mocks/Store.go | 116 +++++++++++++++++++++++++++++++++ pkg/database/querier.go | 2 + pkg/database/share.sql.go | 73 +++++++++++++++++++++ pkg/rest/share.go | 29 +++++++++ pkg/shares/store.go | 58 ++++++++++++++++- sql/postgres/queries/share.sql | 27 ++++++++ 6 files changed, 304 insertions(+), 1 deletion(-) diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index ad4ad0a..8e33522 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -1924,6 +1924,122 @@ func (_c *Store_GetShare_Call) RunAndReturn(run func(context.Context, string) (d return _c } +// GetSharesP provides a mock function with given fields: ctx, arg +func (_m *Store) GetSharesP(ctx context.Context, arg database.GetSharesPParams) ([]database.Share, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for GetSharesP") + } + + var r0 []database.Share + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) ([]database.Share, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) []database.Share); ok { + r0 = rf(ctx, arg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.Share) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, database.GetSharesPParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store_GetSharesP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesP' +type Store_GetSharesP_Call struct { + *mock.Call +} + +// GetSharesP is a helper method to define mock.On call +// - ctx context.Context +// - arg database.GetSharesPParams +func (_e *Store_Expecter) GetSharesP(ctx interface{}, arg interface{}) *Store_GetSharesP_Call { + return &Store_GetSharesP_Call{Call: _e.mock.On("GetSharesP", ctx, arg)} +} + +func (_c *Store_GetSharesP_Call) Run(run func(ctx context.Context, arg database.GetSharesPParams)) *Store_GetSharesP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.GetSharesPParams)) + }) + return _c +} + +func (_c *Store_GetSharesP_Call) Return(_a0 []database.Share, _a1 error) *Store_GetSharesP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Store_GetSharesP_Call) RunAndReturn(run func(context.Context, database.GetSharesPParams) ([]database.Share, error)) *Store_GetSharesP_Call { + _c.Call.Return(run) + return _c +} + +// GetSharesPCount provides a mock function with given fields: ctx, owner +func (_m *Store) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) { + ret := _m.Called(ctx, owner) + + if len(ret) == 0 { + panic("no return value specified for GetSharesPCount") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *int32) (int64, error)); ok { + return rf(ctx, owner) + } + if rf, ok := ret.Get(0).(func(context.Context, *int32) int64); ok { + r0 = rf(ctx, owner) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *int32) error); ok { + r1 = rf(ctx, owner) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store_GetSharesPCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesPCount' +type Store_GetSharesPCount_Call struct { + *mock.Call +} + +// GetSharesPCount is a helper method to define mock.On call +// - ctx context.Context +// - owner *int32 +func (_e *Store_Expecter) GetSharesPCount(ctx interface{}, owner interface{}) *Store_GetSharesPCount_Call { + return &Store_GetSharesPCount_Call{Call: _e.mock.On("GetSharesPCount", ctx, owner)} +} + +func (_c *Store_GetSharesPCount_Call) Run(run func(ctx context.Context, owner *int32)) *Store_GetSharesPCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*int32)) + }) + return _c +} + +func (_c *Store_GetSharesPCount_Call) Return(_a0 int64, _a1 error) *Store_GetSharesPCount_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Store_GetSharesPCount_Call) RunAndReturn(run func(context.Context, *int32) (int64, error)) *Store_GetSharesPCount_Call { + _c.Call.Return(run) + return _c +} + // GetSystemName provides a mock function with given fields: ctx, systemID func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) { ret := _m.Called(ctx, systemID) diff --git a/pkg/database/querier.go b/pkg/database/querier.go index d2cd4a9..c519bed 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -42,6 +42,8 @@ type Querier interface { GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) GetShare(ctx context.Context, id string) (Share, error) + GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share, error) + GetSharesPCount(ctx context.Context, owner *int32) (int64, error) GetSystemName(ctx context.Context, systemID int) (string, error) GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error) GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error) diff --git a/pkg/database/share.sql.go b/pkg/database/share.sql.go index ac7bc38..20a77e1 100644 --- a/pkg/database/share.sql.go +++ b/pkg/database/share.sql.go @@ -79,6 +79,79 @@ func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) { return i, err } +const getSharesP = `-- name: GetSharesP :many +SELECT + s.id, + s.entity_type, + s.entity_id, + s.entity_date, + s.owner, + s.expiration +FROM shares s +WHERE +CASE WHEN $1::INTEGER IS NOT NULL THEN + s.owner = $1 ELSE TRUE END +ORDER BY +CASE WHEN $2::TEXT = 'asc' THEN s.entity_date END ASC, +CASE WHEN $2::TEXT = 'desc' THEN s.entity_date END DESC +OFFSET $3 ROWS +FETCH NEXT $4 ROWS ONLY +` + +type GetSharesPParams struct { + Owner *int32 `json:"owner"` + Direction string `json:"direction"` + Offset int32 `json:"offset"` + PerPage int32 `json:"perPage"` +} + +func (q *Queries) GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share, error) { + rows, err := q.db.Query(ctx, getSharesP, + arg.Owner, + arg.Direction, + arg.Offset, + arg.PerPage, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Share + for rows.Next() { + var i Share + if err := rows.Scan( + &i.ID, + &i.EntityType, + &i.EntityID, + &i.EntityDate, + &i.Owner, + &i.Expiration, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getSharesPCount = `-- name: GetSharesPCount :one +SELECT COUNT(*) +FROM shares s +WHERE +CASE WHEN $1::INTEGER IS NOT NULL THEN + s.owner = $1 ELSE TRUE END +` + +func (q *Queries) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) { + row := q.db.QueryRow(ctx, getSharesPCount, owner) + var count int64 + err := row.Scan(&count) + return count, err +} + const pruneShares = `-- name: PruneShares :exec DELETE FROM shares WHERE expiration < NOW() ` diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 07a161d..45d676e 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -121,6 +121,7 @@ func (sa *shareAPI) Subrouter() http.Handler { r.Post(`/create`, sa.createShare) r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, sa.deleteShare) + r.Post(`/`, sa.listShares) return r } @@ -156,6 +157,34 @@ func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) { respond(w, r, sh) } +func (sa *shareAPI) listShares(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + shs := shares.FromCtx(ctx) + + p := shares.SharesParams{} + err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + + shRes, count, err := shs.Shares(ctx, p) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + response := struct { + Shares []*shares.Share `json:"shares"` + TotalCount int `json:"totalCount"` + }{ + Shares: shRes, + TotalCount: count, + } + + respond(w, r, &response) +} + func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) { ctx := r.Context() shs := shares.FromCtx(ctx) diff --git a/pkg/shares/store.go b/pkg/shares/store.go index 14287a7..46dd3e6 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -3,7 +3,9 @@ package shares import ( "context" "errors" + "fmt" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/rbac" @@ -12,13 +14,21 @@ import ( "github.com/jackc/pgx/v5" ) +type SharesParams struct { + common.Pagination + Direction *common.SortDirection `json:"dir"` +} + type Shares interface { // NewShare creates a new share. NewShare(ctx context.Context, sh CreateShareParams) (*Share, error) - // Share retreives a share record. + // Share retrieves a share record. GetShare(ctx context.Context, id string) (*Share, error) + // Shares retrieves shares visible by the context Subject. + Shares(ctx context.Context, p SharesParams) (shares []*Share, totalCount int, err error) + // Create stores a new share record. Create(ctx context.Context, share *Share) error @@ -98,6 +108,52 @@ func (s *postgresStore) Delete(ctx context.Context, id string) error { return database.FromCtx(ctx).DeleteShare(ctx, id) } +func (s *postgresStore) Shares(ctx context.Context, p SharesParams) (shares []*Share, totalCount int, err error) { + sub := entities.SubjectFrom(ctx) + + // ersatz RBAC + owner := common.PtrTo(int32(-1)) // invalid UID + switch s := sub.(type) { + case *users.User: + if !s.IsAdmin { + owner = s.ID.Int32Ptr() + } else { + owner = nil + } + case *entities.SystemServiceSubject: + owner = nil + default: + return nil, 0, rbac.ErrAccessDenied(rbac.ErrNotAuthorized) + } + + db := database.FromCtx(ctx) + + count, err := db.GetSharesPCount(ctx, owner) + if err != nil { + return nil, 0, fmt.Errorf("shares count: %w", err) + } + + offset, perPage := p.Pagination.OffsetPerPage(100) + dbParam := database.GetSharesPParams{ + Owner: owner, + Direction: p.Direction.DirString(common.DirAsc), + Offset: offset, + PerPage: perPage, + } + + shs, err := db.GetSharesP(ctx, dbParam) + if err != nil { + return nil, 0, err + } + + shares = make([]*Share, 0, len(shs)) + for _, v := range shs { + shares = append(shares, recToShare(v)) + } + + return shares, int(count), nil +} + func (s *postgresStore) Prune(ctx context.Context) error { return database.FromCtx(ctx).PruneShares(ctx) } diff --git a/sql/postgres/queries/share.sql b/sql/postgres/queries/share.sql index 7854c48..c1e940b 100644 --- a/sql/postgres/queries/share.sql +++ b/sql/postgres/queries/share.sql @@ -24,3 +24,30 @@ DELETE FROM shares WHERE id = @id; -- name: PruneShares :exec DELETE FROM shares WHERE expiration < NOW(); + +-- name: GetSharesP :many +SELECT + s.id, + s.entity_type, + s.entity_id, + s.entity_date, + s.owner, + s.expiration +FROM shares s +WHERE +CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN + s.owner = @owner ELSE TRUE END +ORDER BY +CASE WHEN @direction::TEXT = 'asc' THEN s.entity_date END ASC, +CASE WHEN @direction::TEXT = 'desc' THEN s.entity_date END DESC +OFFSET sqlc.arg('offset') ROWS +FETCH NEXT sqlc.arg('per_page') ROWS ONLY +; + +-- name: GetSharesPCount :one +SELECT COUNT(*) +FROM shares s +WHERE +CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN + s.owner = @owner ELSE TRUE END +; -- 2.48.1 From b6ad5e5f8c9c09992e5d03aa417be39b72bbb277 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 11 Feb 2025 08:25:09 -0500 Subject: [PATCH 50/58] Include date --- pkg/shares/share.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/shares/share.go b/pkg/shares/share.go index 6184c02..20a8a6e 100644 --- a/pkg/shares/share.go +++ b/pkg/shares/share.go @@ -47,7 +47,7 @@ func (et EntityType) IsValid() bool { type Share struct { ID string `json:"id"` Type EntityType `json:"entityType"` - Date *jsontypes.Time `json:"-"` // we handle this for the user + Date *jsontypes.Time `json:"entityDate,omitempty"` // we handle this for the user Owner users.UserID `json:"owner"` EntityID uuid.UUID `json:"entityID"` Expiration *jsontypes.Time `json:"expiration"` -- 2.48.1 From 61d1875b63308c00b4e5b2dea2a7317334fb0eda Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 13 Feb 2025 08:13:07 -0500 Subject: [PATCH 51/58] Public subject --- pkg/auth/jwt.go | 17 +++++++++++++---- pkg/rbac/entities/entities.go | 5 +++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index fc20edf..2eae750 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -94,8 +94,16 @@ func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler { return } - if token != nil && jwt.Validate(token, a.jwt.ValidateOptions()...) == nil { - ctx := r.Context() + ctx := r.Context() + + if token != nil { + err := jwt.Validate(token, a.jwt.ValidateOptions()...) + if err != nil { + err = jwtauth.ErrorReason(err) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + username := token.Subject() sub, err := users.FromCtx(ctx).GetUser(ctx, username) @@ -111,8 +119,9 @@ func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler { return } - // Token is authenticated, pass it through - next.ServeHTTP(w, r) + // Public subject + ctx = entities.CtxWithSubject(ctx, entities.NewPublicSubject(r)) + next.ServeHTTP(w, r.WithContext(ctx)) } return http.HandlerFunc(hfn) } diff --git a/pkg/rbac/entities/entities.go b/pkg/rbac/entities/entities.go index eca0217..89553c0 100644 --- a/pkg/rbac/entities/entities.go +++ b/pkg/rbac/entities/entities.go @@ -2,6 +2,7 @@ package entities import ( "context" + "net/http" "github.com/el-mike/restrict/v2" ) @@ -66,6 +67,10 @@ func (s *PublicSubject) GetRoles() []string { return []string{RolePublic} } +func NewPublicSubject(r *http.Request) *PublicSubject { + return &PublicSubject{RemoteAddr: r.RemoteAddr} +} + type SystemServiceSubject struct { Name string } -- 2.48.1 From a0ad25ec4c13218bed7a3f73f2fc6eff887dd682 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 13 Feb 2025 15:55:02 -0500 Subject: [PATCH 52/58] Copyright --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index a136513..4ba46ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2024 Daniel Ponte. +Copyright (c) 2024, 2025 Daniel Ponte. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 2b5c5ca..ae3c024 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ protobuf best practices (i.e. not changing field numbers). ## License and Copyright -© 2024, Daniel Ponte +© 2024, 2025 Daniel Ponte Licensed under the 3-clause BSD license. See LICENSE for details. -- 2.48.1 From e7f96ea58ebff64cbc39972e8a13ba0fc79013af Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 13 Feb 2025 19:54:22 -0500 Subject: [PATCH 53/58] Improve subject in context --- pkg/auth/jwt.go | 16 ++++++++++++---- pkg/rbac/entities/entities.go | 6 +++--- pkg/server/routes.go | 8 +++++--- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 2eae750..a798a63 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -34,8 +34,8 @@ type jwtAuth interface { // InstallVerifyMiddleware installs the JWT verifier middleware to the provided chi Router. VerifyMiddleware() func(http.Handler) http.Handler - // InstallAuthMiddleware installs the JWT authenticator middleware to the provided chi Router. - AuthMiddleware() func(http.Handler) http.Handler + // SubjectMiddleware sets the request context subject from JWT or public. + SubjectMiddleware(requireAuth bool) func(http.Handler) http.Handler // PublicRoutes installs the auth route to the provided chi Router. PublicRoutes(chi.Router) @@ -84,12 +84,20 @@ func TokenFromCookie(r *http.Request) string { return cookie.Value } -func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler { +func (a *Auth) PublicSubjectMiddleware() func(http.Handler) http.Handler { + return a.SubjectMiddleware(false) +} + +func (a *Auth) AuthorizedSubjectMiddleware() func(http.Handler) http.Handler { + return a.SubjectMiddleware(true) +} + +func (a *Auth) SubjectMiddleware(requireToken bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { hfn := func(w http.ResponseWriter, r *http.Request) { token, _, err := jwtauth.FromContext(r.Context()) - if err != nil { + if err != nil && requireToken { http.Error(w, err.Error(), http.StatusUnauthorized) return } diff --git a/pkg/rbac/entities/entities.go b/pkg/rbac/entities/entities.go index 89553c0..2885930 100644 --- a/pkg/rbac/entities/entities.go +++ b/pkg/rbac/entities/entities.go @@ -31,11 +31,11 @@ const ( func SubjectFrom(ctx context.Context) Subject { sub, ok := ctx.Value(SubjectCtxKey).(Subject) - if ok { - return sub + if !ok { + panic("no subject in context") } - return new(PublicSubject) + return sub } type Subject interface { diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 9410baf..e0fbfff 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -29,10 +29,11 @@ func (s *Server) setupRoutes() { r.Use(s.WithCtxStores()) s.installPprof() + r.Use(s.auth.VerifyMiddleware()) r.Group(func(r chi.Router) { + r.Use(s.auth.SubjectMiddleware(true)) // authenticated routes - r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware()) s.nex.PrivateRoutes(r) s.auth.PrivateRoutes(r) s.alerter.PrivateRoutes(r) @@ -41,6 +42,7 @@ func (s *Server) setupRoutes() { r.Group(func(r chi.Router) { s.rateLimit(r) + r.Use(s.auth.SubjectMiddleware(false)) r.Use(render.SetContentType(render.ContentTypeJSON)) // public routes s.sources.PublicRoutes(r) @@ -49,6 +51,7 @@ func (s *Server) setupRoutes() { r.Group(func(r chi.Router) { // auth/share routes get rate-limited heavily, but not using middleware s.rateLimit(r) + r.Use(s.auth.SubjectMiddleware(false)) r.Use(render.SetContentType(render.ContentTypeJSON)) s.auth.PublicRoutes(r) r.Mount("/share", s.rest.ShareRouter()) @@ -56,9 +59,8 @@ func (s *Server) setupRoutes() { r.Group(func(r chi.Router) { s.rateLimit(r) - r.Use(s.auth.VerifyMiddleware()) - // optional auth routes + r.Use(s.auth.SubjectMiddleware(false)) s.clientRoute(r, clientRoot) }) -- 2.48.1 From 2674a71f3022eb54add83e4b867ea88beb322a0b Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 13 Feb 2025 20:12:16 -0500 Subject: [PATCH 54/58] Check for auth, better errors --- pkg/auth/jwt.go | 1 + pkg/server/routes.go | 8 ++++---- pkg/users/store.go | 9 +++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index a798a63..46a45a7 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -116,6 +116,7 @@ func (a *Auth) SubjectMiddleware(requireToken bool) func(http.Handler) http.Hand sub, err := users.FromCtx(ctx).GetUser(ctx, username) if err != nil { + log.Error().Str("username", username).Err(err).Msg("subject middleware get subject") http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index e0fbfff..fcc8c15 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -32,7 +32,7 @@ func (s *Server) setupRoutes() { r.Use(s.auth.VerifyMiddleware()) r.Group(func(r chi.Router) { - r.Use(s.auth.SubjectMiddleware(true)) + r.Use(s.auth.AuthorizedSubjectMiddleware()) // authenticated routes s.nex.PrivateRoutes(r) s.auth.PrivateRoutes(r) @@ -42,7 +42,7 @@ func (s *Server) setupRoutes() { r.Group(func(r chi.Router) { s.rateLimit(r) - r.Use(s.auth.SubjectMiddleware(false)) + r.Use(s.auth.PublicSubjectMiddleware()) r.Use(render.SetContentType(render.ContentTypeJSON)) // public routes s.sources.PublicRoutes(r) @@ -51,7 +51,7 @@ func (s *Server) setupRoutes() { r.Group(func(r chi.Router) { // auth/share routes get rate-limited heavily, but not using middleware s.rateLimit(r) - r.Use(s.auth.SubjectMiddleware(false)) + r.Use(s.auth.PublicSubjectMiddleware()) r.Use(render.SetContentType(render.ContentTypeJSON)) s.auth.PublicRoutes(r) r.Mount("/share", s.rest.ShareRouter()) @@ -60,7 +60,7 @@ func (s *Server) setupRoutes() { r.Group(func(r chi.Router) { s.rateLimit(r) // optional auth routes - r.Use(s.auth.SubjectMiddleware(false)) + r.Use(s.auth.PublicSubjectMiddleware()) s.clientRoute(r, clientRoot) }) diff --git a/pkg/users/store.go b/pkg/users/store.go index 0129181..5722ab2 100644 --- a/pkg/users/store.go +++ b/pkg/users/store.go @@ -2,11 +2,16 @@ package users import ( "context" + "errors" "dynatron.me/x/stillbox/internal/cache" "dynatron.me/x/stillbox/pkg/database" ) +var ( + ErrNoSuchUser = errors.New("no such user") +) + type Store interface { // GetUser gets a user by UID. GetUser(ctx context.Context, username string) (*User, error) @@ -84,6 +89,10 @@ func (s *postgresStore) GetUser(ctx context.Context, username string) (*User, er dbu, err := s.db.GetUserByUsername(ctx, username) if err != nil { + if database.IsNoRows(err) { + return nil, ErrNoSuchUser + } + return nil, err } -- 2.48.1 From 6e156562766200982ffbde58b9d4d7739be97591 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 13 Feb 2025 20:24:57 -0500 Subject: [PATCH 55/58] Fix access denied error --- pkg/rbac/rbac.go | 11 +++++++++-- pkg/rest/api.go | 2 +- pkg/shares/store.go | 2 +- pkg/sources/http.go | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 58da816..3d4cce6 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -12,13 +12,18 @@ import ( var ( ErrBadSubject = errors.New("bad subject in token") + ErrAccessDenied = errors.New("access denied") ) -func ErrAccessDenied(err error) *restrict.AccessDeniedError { +func IsErrAccessDenied(err error) error { if accessErr, ok := err.(*restrict.AccessDeniedError); ok { return accessErr } + if err == ErrAccessDenied { + return err + } + return nil } @@ -115,5 +120,7 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp Context: o.context, } - return sub, r.access.Authorize(req) + authRes := r.access.Authorize(req) + + return sub, authRes } diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 2a02206..6fc0fd8 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -179,7 +179,7 @@ func autoError(err error) render.Renderer { } } - if rbac.ErrAccessDenied(err) != nil { + if rbac.IsErrAccessDenied(err) != nil { return forbiddenErrText(err) } diff --git a/pkg/shares/store.go b/pkg/shares/store.go index 46dd3e6..85cae56 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -123,7 +123,7 @@ func (s *postgresStore) Shares(ctx context.Context, p SharesParams) (shares []*S case *entities.SystemServiceSubject: owner = nil default: - return nil, 0, rbac.ErrAccessDenied(rbac.ErrNotAuthorized) + return nil, 0, rbac.ErrAccessDenied } db := database.FromCtx(ctx) diff --git a/pkg/sources/http.go b/pkg/sources/http.go index dbc51f7..e004621 100644 --- a/pkg/sources/http.go +++ b/pkg/sources/http.go @@ -134,7 +134,7 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) { } err = h.ing.Ingest(entities.CtxWithSubject(ctx, submitterSub), call) if err != nil { - if rbac.ErrAccessDenied(err) != nil { + if rbac.IsErrAccessDenied(err) != nil { log.Error().Err(err).Msg("ingest failed") http.Error(w, "Call ingest failed.", http.StatusForbidden) } -- 2.48.1 From dd2ee06f032cef21e4262db8f4df4dbba386cb97 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 13 Feb 2025 20:37:38 -0500 Subject: [PATCH 56/58] log failed access --- pkg/rbac/entities/entities.go | 10 ++++++++++ pkg/rbac/rbac.go | 13 +++++++++++++ pkg/shares/share.go | 4 ++++ pkg/users/user.go | 4 ++++ 4 files changed, 31 insertions(+) diff --git a/pkg/rbac/entities/entities.go b/pkg/rbac/entities/entities.go index 2885930..42e1548 100644 --- a/pkg/rbac/entities/entities.go +++ b/pkg/rbac/entities/entities.go @@ -2,6 +2,7 @@ package entities import ( "context" + "fmt" "net/http" "github.com/el-mike/restrict/v2" @@ -39,6 +40,7 @@ func SubjectFrom(ctx context.Context) Subject { } type Subject interface { + fmt.Stringer restrict.Subject GetName() string } @@ -63,6 +65,10 @@ func (s *PublicSubject) GetName() string { return "PUBLIC:" + s.RemoteAddr } +func (s *PublicSubject) String() string { + return s.GetName() +} + func (s *PublicSubject) GetRoles() []string { return []string{RolePublic} } @@ -79,6 +85,10 @@ func (s *SystemServiceSubject) GetName() string { return "SYSTEM:" + s.Name } +func (s *SystemServiceSubject) String() string { + return s.GetName() +} + func (s *SystemServiceSubject) GetRoles() []string { return []string{RoleSystem} } diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 3d4cce6..b153885 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -8,6 +8,7 @@ import ( "github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2/adapters" + "github.com/rs/zerolog/log" ) var ( @@ -121,6 +122,18 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp } authRes := r.access.Authorize(req) + if IsErrAccessDenied(authRes) != nil { + subS := "" + resS := "" + if sub != nil { + subS = sub.String() + } + + if res != nil { + resS = res.GetResourceName() + } + log.Error().Str("resource", resS).Strs("actions", req.Actions).Str("subject", subS).Msg("access denied") + } return sub, authRes } diff --git a/pkg/shares/share.go b/pkg/shares/share.go index 20a8a6e..3b75c50 100644 --- a/pkg/shares/share.go +++ b/pkg/shares/share.go @@ -57,6 +57,10 @@ func (s *Share) GetName() string { return "SHARE:" + s.ID } +func (s *Share) String() string { + return s.GetName() +} + func (s *Share) GetRoles() []string { return []string{entities.RoleShareGuest} } diff --git a/pkg/users/user.go b/pkg/users/user.go index 9860be1..b5d5d27 100644 --- a/pkg/users/user.go +++ b/pkg/users/user.go @@ -71,6 +71,10 @@ func (u *User) GetName() string { return u.Username } +func (u *User) String() string { + return "USER:"+u.GetName() +} + func (u *User) GetRoles() []string { r := make([]string, 1, 2) -- 2.48.1 From 48903bb4a21ca181a5707e2062ea268e320c8570 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 14 Feb 2025 00:18:49 -0500 Subject: [PATCH 57/58] shareUI --- .../stillbox/src/app/share/share.service.ts | 28 +++ .../src/app/shares/shares.component.html | 83 +++++++- .../src/app/shares/shares.component.ts | 189 +++++++++++++++++- client/stillbox/src/proxy.conf.json | 4 +- 4 files changed, 298 insertions(+), 6 deletions(-) diff --git a/client/stillbox/src/app/share/share.service.ts b/client/stillbox/src/app/share/share.service.ts index 75d8a1f..4e43b8b 100644 --- a/client/stillbox/src/app/share/share.service.ts +++ b/client/stillbox/src/app/share/share.service.ts @@ -6,6 +6,26 @@ import { CallRecord } from '../calls'; import { Share, ShareType } from '../shares'; import { ActivatedRoute, Router } from '@angular/router'; +export interface ShareRecord { + id: string; + entityType: string; + entityDate: Date; + owner: string; + entityID: string; + expiration: Date | null; +} + +export interface ShareListParams { + page: number | null; + perPage: number | null; + dir: string | null; +} + +export interface Shares { + shares: ShareRecord[]; + totalCount: number; +} + @Injectable({ providedIn: 'root', }) @@ -28,6 +48,14 @@ export class ShareService { return this.http.get(`/share/${id}`); } + deleteShare(id: string): Observable { + return this.http.delete(`/api/share/${id}`); + } + + getShares(p: ShareListParams): Observable { + return this.http.post('/api/share/', p); + } + getSharedItem(s: Observable): Observable { return s.pipe( map((res) => { diff --git a/client/stillbox/src/app/shares/shares.component.html b/client/stillbox/src/app/shares/shares.component.html index 9f6f00b..d1e5fc7 100644 --- a/client/stillbox/src/app/shares/shares.component.html +++ b/client/stillbox/src/app/shares/shares.component.html @@ -1 +1,82 @@ -

shares works!

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + @switch (share.entityType) { + @case ("incident") { + newspaper + } + @case ("call") { + campaign + } + } + Link + link + Date + {{ share.entityDate | fmtDate }} + Owner + {{ share.owner }} + Delete + + delete + +
+
+
+ + +
+ +
+ +
+
diff --git a/client/stillbox/src/app/shares/shares.component.ts b/client/stillbox/src/app/shares/shares.component.ts index 8bbcdea..c95a668 100644 --- a/client/stillbox/src/app/shares/shares.component.ts +++ b/client/stillbox/src/app/shares/shares.component.ts @@ -1,9 +1,192 @@ -import { Component } from '@angular/core'; +import { Component, Pipe, PipeTransform, ViewChild } from '@angular/core'; +import { CommonModule, AsyncPipe } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { + MatPaginator, + MatPaginatorModule, + PageEvent, +} from '@angular/material/paginator'; +import { PrefsService } from '../prefs/prefs.service'; +import { MatIconModule } from '@angular/material/icon'; +import { SelectionModel } from '@angular/cdk/collections'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { TalkgroupService } from '../talkgroups/talkgroups.service'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { debounceTime } from 'rxjs/operators'; +import { ToolbarContextService } from '../navigation/toolbar-context.service'; +import { + ShareListParams, + ShareRecord, + ShareService, +} from '../share/share.service'; +import { FmtDatePipe } from '../incidents/incidents.component'; + +const reqPageSize = 200; @Component({ selector: 'app-shares', - imports: [], + imports: [ + MatIconModule, + MatPaginatorModule, + MatTableModule, + MatFormFieldModule, + ReactiveFormsModule, + FormsModule, + FmtDatePipe, + MatInputModule, + MatCheckboxModule, + CommonModule, + MatProgressSpinnerModule, + ], templateUrl: './shares.component.html', styleUrl: './shares.component.scss', }) -export class SharesComponent {} +export class SharesComponent { + sharesResult = new BehaviorSubject(new Array(0)); + selection = new SelectionModel(true, []); + + @ViewChild('paginator') paginator!: MatPaginator; + count = 0; + curLen = 0; + page = 0; + perPage = 25; + pageSizeOptions = [25, 50, 75, 100, 200]; + columns = ['select', 'type', 'link', 'date', 'owner', 'delete']; + curPage = { pageIndex: 0, pageSize: 0 }; + currentSet!: ShareRecord[]; + currentServerPage = 0; // page is never 0, forces load + isLoading = true; + subscriptions = new Subscription(); + pageWindow = 0; + fetchIncidents = new BehaviorSubject( + this.buildParams(this.curPage, this.curPage.pageIndex), + ); + + constructor(private sharesSvc: ShareService) {} + + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.curLen; + return numSelected === numRows; + } + + buildParams(p: PageEvent, serverPage: number): ShareListParams { + const par: ShareListParams = { + page: serverPage, + perPage: reqPageSize, + dir: 'asc', + }; + + return par; + } + + masterToggle() { + this.isAllSelected() + ? this.selection.clear() + : this.sharesResult.value.forEach((row) => this.selection.select(row)); + } + + setPage(p: PageEvent, force?: boolean) { + this.selection.clear(); + this.curPage = p; + if (p && p!.pageSize != this.perPage) { + this.perPage = p!.pageSize; + } + this.getShares(p, force); + } + + refresh() { + this.selection.clear(); + this.getShares(this.curPage, true); + } + + getShares(p: PageEvent, force?: boolean) { + const pageStart = p.pageIndex * p.pageSize; + const serverPage = Math.floor(pageStart / reqPageSize) + 1; + this.pageWindow = pageStart % reqPageSize; + if (serverPage == this.currentServerPage && !force && this.currentSet) { + this.sharesResult.next( + this.sharesResult + ? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize) + : [], + ); + } else { + this.currentServerPage = serverPage; + this.fetchIncidents.next(this.buildParams(p, serverPage)); + } + } + + zeroPage(): PageEvent { + return { + pageIndex: 0, + pageSize: this.curPage.pageSize, + }; + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + ngOnInit() { + let cpp = 25; + this.perPage = cpp; + + this.setPage({ + pageIndex: 0, + pageSize: cpp, + }); + this.subscriptions.add( + this.fetchIncidents + .pipe( + switchMap((params) => { + return this.sharesSvc.getShares(params); + }), + ) + .subscribe((shares) => { + this.isLoading = false; + this.count = shares.totalCount; + this.currentSet = shares.shares; + this.sharesResult.next( + this.currentSet + ? this.currentSet.slice( + this.pageWindow, + this.pageWindow + this.perPage, + ) + : [], + ); + }), + ); + this.subscriptions.add( + this.sharesResult.subscribe((cr) => { + this.curLen = cr.length; + }), + ); + } + + deleteShare(shareID: string) { + if (confirm('Are you sure you want to delete this share?')) { + this.sharesSvc.deleteShare(shareID).subscribe({ + next: () => { + this.fetchIncidents.next( + this.buildParams(this.curPage, this.curPage.pageIndex), + ); + }, + error: (err) => { + alert(err); + }, + }); + } + } +} diff --git a/client/stillbox/src/proxy.conf.json b/client/stillbox/src/proxy.conf.json index 67915a8..0a8777d 100644 --- a/client/stillbox/src/proxy.conf.json +++ b/client/stillbox/src/proxy.conf.json @@ -1,9 +1,9 @@ { - "/api": { + "/api/": { "target": "http://xenon:3050", "secure": false }, - "/share": { + "/share/": { "target": "http://xenon:3050", "secure": false } -- 2.48.1 From 68d4e3e269930d3965325eb522f08a5b90a30650 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 14 Feb 2025 00:19:24 -0500 Subject: [PATCH 58/58] share UI backend --- pkg/database/mocks/Store.go | 14 +++++----- pkg/database/querier.go | 2 +- pkg/database/share.sql.go | 49 ++++++++++++++++++---------------- pkg/shares/share.go | 3 ++- pkg/shares/store.go | 4 ++- sql/postgres/queries/share.sql | 25 ++++++++--------- 6 files changed, 50 insertions(+), 47 deletions(-) diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index 8e33522..be3d593 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -1925,23 +1925,23 @@ func (_c *Store_GetShare_Call) RunAndReturn(run func(context.Context, string) (d } // GetSharesP provides a mock function with given fields: ctx, arg -func (_m *Store) GetSharesP(ctx context.Context, arg database.GetSharesPParams) ([]database.Share, error) { +func (_m *Store) GetSharesP(ctx context.Context, arg database.GetSharesPParams) ([]database.GetSharesPRow, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for GetSharesP") } - var r0 []database.Share + var r0 []database.GetSharesPRow var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) ([]database.Share, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) ([]database.GetSharesPRow, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) []database.Share); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) []database.GetSharesPRow); ok { r0 = rf(ctx, arg) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]database.Share) + r0 = ret.Get(0).([]database.GetSharesPRow) } } @@ -1973,12 +1973,12 @@ func (_c *Store_GetSharesP_Call) Run(run func(ctx context.Context, arg database. return _c } -func (_c *Store_GetSharesP_Call) Return(_a0 []database.Share, _a1 error) *Store_GetSharesP_Call { +func (_c *Store_GetSharesP_Call) Return(_a0 []database.GetSharesPRow, _a1 error) *Store_GetSharesP_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *Store_GetSharesP_Call) RunAndReturn(run func(context.Context, database.GetSharesPParams) ([]database.Share, error)) *Store_GetSharesP_Call { +func (_c *Store_GetSharesP_Call) RunAndReturn(run func(context.Context, database.GetSharesPParams) ([]database.GetSharesPRow, error)) *Store_GetSharesP_Call { _c.Call.Return(run) return _c } diff --git a/pkg/database/querier.go b/pkg/database/querier.go index c519bed..36e68d7 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -42,7 +42,7 @@ type Querier interface { GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) GetShare(ctx context.Context, id string) (Share, error) - GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share, error) + GetSharesP(ctx context.Context, arg GetSharesPParams) ([]GetSharesPRow, error) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) GetSystemName(ctx context.Context, systemID int) (string, error) GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error) diff --git a/pkg/database/share.sql.go b/pkg/database/share.sql.go index 20a77e1..081f6b6 100644 --- a/pkg/database/share.sql.go +++ b/pkg/database/share.sql.go @@ -55,14 +55,14 @@ func (q *Queries) DeleteShare(ctx context.Context, id string) error { const getShare = `-- name: GetShare :one SELECT - id, - entity_type, - entity_id, - entity_date, - owner, - expiration -FROM shares -WHERE id = $1 + s.id, + s.entity_type, + s.entity_id, + s.entity_date, + s.owner, + s.expiration +FROM shares s +WHERE s.id = $1 ` func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) { @@ -81,13 +81,10 @@ func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) { const getSharesP = `-- name: GetSharesP :many SELECT - s.id, - s.entity_type, - s.entity_id, - s.entity_date, - s.owner, - s.expiration + s.id, s.entity_type, s.entity_id, s.entity_date, s.owner, s.expiration, + u.username FROM shares s +JOIN users u ON (s.owner = u.id) WHERE CASE WHEN $1::INTEGER IS NOT NULL THEN s.owner = $1 ELSE TRUE END @@ -105,7 +102,12 @@ type GetSharesPParams struct { PerPage int32 `json:"perPage"` } -func (q *Queries) GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share, error) { +type GetSharesPRow struct { + Share Share `json:"share"` + Username string `json:"username"` +} + +func (q *Queries) GetSharesP(ctx context.Context, arg GetSharesPParams) ([]GetSharesPRow, error) { rows, err := q.db.Query(ctx, getSharesP, arg.Owner, arg.Direction, @@ -116,16 +118,17 @@ func (q *Queries) GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share return nil, err } defer rows.Close() - var items []Share + var items []GetSharesPRow for rows.Next() { - var i Share + var i GetSharesPRow if err := rows.Scan( - &i.ID, - &i.EntityType, - &i.EntityID, - &i.EntityDate, - &i.Owner, - &i.Expiration, + &i.Share.ID, + &i.Share.EntityType, + &i.Share.EntityID, + &i.Share.EntityDate, + &i.Share.Owner, + &i.Share.Expiration, + &i.Username, ); err != nil { return nil, err } diff --git a/pkg/shares/share.go b/pkg/shares/share.go index 3b75c50..67abc02 100644 --- a/pkg/shares/share.go +++ b/pkg/shares/share.go @@ -48,7 +48,8 @@ type Share struct { ID string `json:"id"` Type EntityType `json:"entityType"` Date *jsontypes.Time `json:"entityDate,omitempty"` // we handle this for the user - Owner users.UserID `json:"owner"` + Owner users.UserID `json:"-"` + OwnerUser *string `json:"owner,omitempty"` EntityID uuid.UUID `json:"entityID"` Expiration *jsontypes.Time `json:"expiration"` } diff --git a/pkg/shares/store.go b/pkg/shares/store.go index 85cae56..d0f1313 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -148,7 +148,9 @@ func (s *postgresStore) Shares(ctx context.Context, p SharesParams) (shares []*S shares = make([]*Share, 0, len(shs)) for _, v := range shs { - shares = append(shares, recToShare(v)) + s := recToShare(v.Share) + s.OwnerUser = &v.Username + shares = append(shares, s) } return shares, int(count), nil diff --git a/sql/postgres/queries/share.sql b/sql/postgres/queries/share.sql index c1e940b..08e9bce 100644 --- a/sql/postgres/queries/share.sql +++ b/sql/postgres/queries/share.sql @@ -1,13 +1,13 @@ -- name: GetShare :one SELECT - id, - entity_type, - entity_id, - entity_date, - owner, - expiration -FROM shares -WHERE id = @id; + s.id, + s.entity_type, + s.entity_id, + s.entity_date, + s.owner, + s.expiration +FROM shares s +WHERE s.id = @id; -- name: CreateShare :exec INSERT INTO shares ( @@ -27,13 +27,10 @@ DELETE FROM shares WHERE expiration < NOW(); -- name: GetSharesP :many SELECT - s.id, - s.entity_type, - s.entity_id, - s.entity_date, - s.owner, - s.expiration + sqlc.embed(s), + u.username FROM shares s +JOIN users u ON (s.owner = u.id) WHERE CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN s.owner = @owner ELSE TRUE END -- 2.48.1