diff --git a/client/stillbox/src/app/calls.ts b/client/stillbox/src/app/calls.ts index a38502f..2d0b89c 100644 --- a/client/stillbox/src/app/calls.ts +++ b/client/stillbox/src/app/calls.ts @@ -4,6 +4,7 @@ export interface CallRecord { audioURL: string | null; duration: number; systemId: number; + talkerAlias: string | null; tgid: number; incidents: number; // in incident } diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index 86ce14f..c7c6105 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -168,6 +168,10 @@ > + + Source + + Duration diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 030180f..19a8ab5 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, inject, ViewChild } from '@angular/core'; import { CommonModule, AsyncPipe } from '@angular/common'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatTable, MatTableModule } from '@angular/material/table'; +import { MatTableModule } from '@angular/material/table'; import { MatPaginator, MatPaginatorModule, @@ -19,6 +19,7 @@ import { DatePipe, DownloadURLPipe, FixedPointPipe, + TalkerPipe, TalkgroupPipe, TimePipe, } from './calls.service'; @@ -57,6 +58,7 @@ const reqPageSize = 200; MatIconModule, FixedPointPipe, TalkgroupPipe, + TalkerPipe, TimePipe, DatePipe, MatPaginatorModule, @@ -95,6 +97,7 @@ export class CallsComponent { 'system', 'group', 'talkgroup', + 'talker', 'duration', ]; curPage = { pageIndex: 0, pageSize: 0 }; diff --git a/client/stillbox/src/app/calls/calls.service.ts b/client/stillbox/src/app/calls/calls.service.ts index 570563f..a90ed08 100644 --- a/client/stillbox/src/app/calls/calls.service.ts +++ b/client/stillbox/src/app/calls/calls.service.ts @@ -27,6 +27,21 @@ export class DatePipe implements PipeTransform { } } +@Pipe({ + name: 'talker', + standalone: true, + pure: true, +}) +export class TalkerPipe implements PipeTransform { + transform(call: CallRecord, args?: any): string { + if (call.talkerAlias != null) { + return call.talkerAlias; + } + + return '—'; + } +} + @Pipe({ name: 'time', standalone: true, diff --git a/client/stillbox/src/app/charts/charts.component.html b/client/stillbox/src/app/charts/charts.component.html index 439281b..db578cf 100644 --- a/client/stillbox/src/app/charts/charts.component.html +++ b/client/stillbox/src/app/charts/charts.component.html @@ -1 +1,2 @@
+
diff --git a/client/stillbox/src/app/charts/charts.component.ts b/client/stillbox/src/app/charts/charts.component.ts index 707ed5e..3573e5d 100644 --- a/client/stillbox/src/app/charts/charts.component.ts +++ b/client/stillbox/src/app/charts/charts.component.ts @@ -1,16 +1,18 @@ import { Component, ElementRef, Input } from '@angular/core'; import * as d3 from 'd3'; import { CallsService } from '../calls/calls.service'; -import { CallStatsRecord } from '../calls'; +import { Observable, switchMap } from 'rxjs'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { NgIf } from '@angular/common'; @Component({ selector: 'chart', - imports: [], + imports: [NgIf, MatProgressSpinnerModule], templateUrl: './charts.component.html', styleUrl: './charts.component.scss', }) export class ChartsComponent { - @Input() interval!: string; + @Input() interval!: Observable; loading = true; // I hate javascript so much months = [ @@ -32,8 +34,8 @@ export class ChartsComponent { private callsSvc: CallsService, ) {} - dateFormat(d: Date): string { - switch (this.interval) { + dateFormat(d: Date, interval: string): string { + switch (interval) { case 'month': return `${this.months[d.getMonth()]} ${d.getFullYear()}`; case 'day': @@ -45,70 +47,87 @@ export class ChartsComponent { } ngOnInit() { - this.callsSvc.getCallStats(this.interval).subscribe((stats) => { - let cMax = 0; - var cMin = 0; - let data = stats.stats.map((rec) => { - if (cMin == 0 && rec.count > cMin) { - cMin = rec.count; - } - if (rec.count < cMin) { - cMin = rec.count; - } - if (rec.count > cMax) { - cMax = rec.count; - } - return { count: rec.count, time: this.dateFormat(new Date(rec.time)) }; - }); - // set the dimensions and margins of the graph - var margin = { top: 30, right: 30, bottom: 70, left: 60 }, - width = 460 - margin.left - margin.right, - height = 400 - margin.top - margin.bottom; - const svg = d3 - .select(this.elementRef.nativeElement) - .select('.chart') - .append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - // X axis - var x = d3 - .scaleBand() - .range([0, width]) - .domain( - data.map(function (d) { - return d.time; - }), - ) - .padding(0.2); - svg - .append('g') - .attr('transform', 'translate(0,' + height + ')') - .call(d3.axisBottom(x)) - .selectAll('text') - .attr('transform', 'translate(-10,0)rotate(-45)') - .style('text-anchor', 'end'); - - // Add Y axis - var y = d3.scaleLinear().domain([0, cMax]).range([height, 0]); - svg.append('g').call(d3.axisLeft(y)); - svg - .selectAll('mybar') - .data(data) - .enter() - .append('rect') - .attr('x', (d) => x(d.time)!) - .attr('y', function (d) { - return y(d.count); - }) - .attr('width', x.bandwidth()) - .attr('height', function (d) { - return height - y(d.count); - }) - .attr('fill', function (d) { - return d3.interpolateTurbo((d.count - cMin) / (cMax - cMin)); + this.interval + .pipe( + switchMap((intv) => { + d3.select(this.elementRef.nativeElement).select('.chart').html(''); + this.loading = true; + return this.callsSvc.getCallStats(intv); + }), + ) + .subscribe((stats) => { + let cMax = 0; + var cMin = 0; + let data = stats.stats.map((rec) => { + if (cMin == 0 && rec.count > cMin) { + cMin = rec.count; + } + if (rec.count < cMin) { + cMin = rec.count; + } + if (rec.count > cMax) { + cMax = rec.count; + } + return { + count: rec.count, + time: this.dateFormat(new Date(rec.time), stats.interval), + }; }); - }); + // set the dimensions and margins of the graph + var margin = { top: 30, right: 30, bottom: 70, left: 60 }, + width = 460 - margin.left - margin.right, + height = 400 - margin.top - margin.bottom; + // clear the old one + d3.select(this.elementRef.nativeElement).select('.chart').html(''); + const svg = d3 + .select(this.elementRef.nativeElement) + .select('.chart') + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr( + 'transform', + 'translate(' + margin.left + ',' + margin.top + ')', + ); + // X axis + var x = d3 + .scaleBand() + .range([0, width]) + .domain( + data.map(function (d) { + return d.time; + }), + ) + .padding(0.2); + svg + .append('g') + .attr('transform', 'translate(0,' + height + ')') + .call(d3.axisBottom(x)) + .selectAll('text') + .attr('transform', 'translate(-10,0)rotate(-45)') + .style('text-anchor', 'end'); + + // Add Y axis + var y = d3.scaleLinear().domain([0, cMax]).range([height, 0]); + svg.append('g').call(d3.axisLeft(y)); + svg + .selectAll('mybar') + .data(data) + .enter() + .append('rect') + .attr('x', (d) => x(d.time)!) + .attr('y', function (d) { + return y(d.count); + }) + .attr('width', x.bandwidth()) + .attr('height', function (d) { + return height - y(d.count); + }) + .attr('fill', function (d) { + return d3.interpolateTurbo((d.count - cMin) / (cMax - cMin)); + }); + this.loading = false; + }); } } diff --git a/client/stillbox/src/app/home/home.component.html b/client/stillbox/src/app/home/home.component.html index 191617f..8d74aa4 100644 --- a/client/stillbox/src/app/home/home.component.html +++ b/client/stillbox/src/app/home/home.component.html @@ -1,4 +1,14 @@ -
Calls By Week
- +
+ + + + Hour + Day + Week + Month + + +
+
diff --git a/client/stillbox/src/app/home/home.component.scss b/client/stillbox/src/app/home/home.component.scss index 06e84df..e78fb2f 100644 --- a/client/stillbox/src/app/home/home.component.scss +++ b/client/stillbox/src/app/home/home.component.scss @@ -1,6 +1,6 @@ mat-card.chart { width: 500px; - height: 400px; + height: 500px; margin: 30px 30px 40px 40px; } diff --git a/client/stillbox/src/app/home/home.component.ts b/client/stillbox/src/app/home/home.component.ts index 0c5d88d..ad4b230 100644 --- a/client/stillbox/src/app/home/home.component.ts +++ b/client/stillbox/src/app/home/home.component.ts @@ -1,11 +1,50 @@ -import { Component } from '@angular/core'; +import { Component, computed, signal, ViewChild } from '@angular/core'; import { ChartsComponent } from '../charts/charts.component'; import { MatCardModule } from '@angular/material/card'; +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + debounceTime, + filter, + Observable, + startWith, + switchAll, + switchMap, +} from 'rxjs'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; @Component({ selector: 'app-home', - imports: [ChartsComponent, MatCardModule], + imports: [ + ChartsComponent, + MatCardModule, + MatFormFieldModule, + MatSelectModule, + MatInputModule, + FormsModule, + ReactiveFormsModule, + ], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) -export class HomeComponent {} +export class HomeComponent { + form = new FormGroup({ + callsPer: new FormControl(), + }); + callsOb!: Observable; + + ngOnInit() { + this.form.controls['callsPer'].setValue('week'); + this.callsOb = this.form.controls['callsPer'].valueChanges.pipe( + startWith('week'), + debounceTime(300), + filter((val) => val !== null), + ); + } +} diff --git a/internal/common/intervals.go b/internal/common/intervals.go new file mode 100644 index 0000000..0458f3c --- /dev/null +++ b/internal/common/intervals.go @@ -0,0 +1,84 @@ +package common + +import ( + "time" +) + +const ( + DaysInWeek = 7 + MonthsInQuarter = 3 +) + +type TimeBounder interface { + GetDailyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetWeeklyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetMonthlyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetQuarterlyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetYearlyBounds(date time.Time) (lowerBound, upperBound time.Time) +} + +type tbOpt func(*timeBounder) + +func WithLocation(l *time.Location) tbOpt { + return func(tb *timeBounder) { + tb.loc = l + } +} + +func NewTimeBounder(opts ...tbOpt) timeBounder { + tb := timeBounder{} + + for _, opt := range opts { + opt(&tb) + } + + if tb.loc == nil { + tb.loc = time.UTC + } + + return tb +} + +type timeBounder struct { + loc *time.Location +} + +func (tb timeBounder) GetDailyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(0, 0, 1) + + return +} + +func (tb timeBounder) GetWeeklyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, tb.loc).AddDate(0, 0, -int(date.Weekday()-time.Monday)) + upperBound = lowerBound.AddDate(0, 0, DaysInWeek) + + return +} + +func (tb timeBounder) GetMonthlyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(0, 1, 0) + + return +} + +func (tb *timeBounder) GetQuarterlyBounds(date time.Time) (lowerBound, upperBound time.Time) { + year, _, _ := date.Date() + + quarter := (int(date.Month()) - 1) / MonthsInQuarter + firstMonthOfTheQuarter := time.Month(quarter*MonthsInQuarter + 1) + + lowerBound = time.Date(year, firstMonthOfTheQuarter, 1, 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(0, MonthsInQuarter, 0) + + return +} + +func (tb timeBounder) GetYearlyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), 1, 1, 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(1, 0, 0) + + return +} diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index 72b39e8..6247669 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -182,7 +182,7 @@ func (as *alerter) eval(ctx context.Context, now time.Time, testMode bool) ([]al origScore := s.Score tgr, err := as.tgCache.TG(ctx, s.ID) if err != nil { - log.Error().Err(err).Msg("alerting eval tg get") + log.Debug().Str("tg", s.ID.String()).Err(err).Msg("alerting eval tg get") continue } diff --git a/pkg/calls/call.go b/pkg/calls/call.go index 44f0702..06b1951 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -91,7 +91,17 @@ func (c *Call) GetResourceName() string { } func (c *Call) String() string { - return fmt.Sprintf("%s to %d from %d", c.AudioName, c.Talkgroup, c.Source) + var from string + switch { + case c.Source != 0 && c.TalkerAlias != nil: + from = fmt.Sprintf(" from %s (%d)", *c.TalkerAlias, c.Source) + case c.Source != 0: + from = fmt.Sprintf(" from %d", c.Source) + case c.TalkerAlias != nil: + from = fmt.Sprintf(" from %s", *c.TalkerAlias) + } + + return fmt.Sprintf("%s to %d%s", c.AudioName, c.Talkgroup, from) } func (c *Call) ShouldStore() bool { diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 14e9716..e405ec2 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -204,6 +204,7 @@ type CallsParams struct { TagsNot []string `json:"tagsNot"` TGFilter *string `json:"tgFilter"` AtLeastSeconds *float32 `json:"atLeastSeconds"` + UnknownTG bool `json:"unknownTG"` } func (s *postgresStore) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) { @@ -224,6 +225,7 @@ func (s *postgresStore) Calls(ctx context.Context, p CallsParams) (rows []databa PerPage: perPage, Direction: p.Direction.DirString(common.DirAsc), TGFilter: p.TGFilter, + UnknownTG: p.UnknownTG, } if p.AtLeastSeconds != nil { @@ -245,6 +247,7 @@ func (s *postgresStore) Calls(ctx context.Context, p CallsParams) (rows []databa TagsNot: par.TagsNot, TGFilter: par.TGFilter, LongerThan: par.LongerThan, + UnknownTG: par.UnknownTG, }) if err != nil { return err diff --git a/pkg/database/calls.sql.go b/pkg/database/calls.sql.go index 45d1702..120110f 100644 --- a/pkg/database/calls.sql.go +++ b/pkg/database/calls.sql.go @@ -319,6 +319,9 @@ CASE WHEN $4::TEXT[] IS NOT NULL THEN ) ELSE TRUE END) AND (CASE WHEN $6::NUMERIC IS NOT NULL THEN ( c.duration > $6 + ) ELSE TRUE END) AND +(CASE WHEN $7::BOOLEAN = TRUE THEN ( + tgs.tgid IS NULL ) ELSE TRUE END) ` @@ -329,6 +332,7 @@ type ListCallsCountParams struct { TagsNot []string `json:"tagsNot"` TGFilter *string `json:"tgFilter"` LongerThan pgtype.Numeric `json:"longerThan"` + UnknownTG bool `json:"unknownTg"` } func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error) { @@ -339,6 +343,7 @@ func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams) arg.TagsNot, arg.TGFilter, arg.LongerThan, + arg.UnknownTG, ) var count int64 err := row.Scan(&count) @@ -374,13 +379,16 @@ CASE WHEN $4::TEXT[] IS NOT NULL THEN ) ELSE TRUE END) AND (CASE WHEN $6::NUMERIC IS NOT NULL THEN ( c.duration > $6 + ) ELSE TRUE END) AND +(CASE WHEN $7::BOOLEAN = TRUE THEN ( + tgs.tgid IS NULL ) ELSE TRUE END) GROUP BY c.id, c.call_date ORDER BY -CASE WHEN $7::TEXT = 'asc' THEN c.call_date END ASC, -CASE WHEN $7 = 'desc' THEN c.call_date END DESC -OFFSET $8 ROWS -FETCH NEXT $9 ROWS ONLY +CASE WHEN $8::TEXT = 'asc' THEN c.call_date END ASC, +CASE WHEN $8 = 'desc' THEN c.call_date END DESC +OFFSET $9 ROWS +FETCH NEXT $10 ROWS ONLY ` type ListCallsPParams struct { @@ -390,6 +398,7 @@ type ListCallsPParams struct { TagsNot []string `json:"tagsNot"` TGFilter *string `json:"tgFilter"` LongerThan pgtype.Numeric `json:"longerThan"` + UnknownTG bool `json:"unknownTg"` Direction string `json:"direction"` Offset int32 `json:"offset"` PerPage int32 `json:"perPage"` @@ -413,6 +422,7 @@ func (q *Queries) ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListC arg.TagsNot, arg.TGFilter, arg.LongerThan, + arg.UnknownTG, arg.Direction, arg.Offset, arg.PerPage, diff --git a/pkg/database/partman/intervals.go b/pkg/database/partman/intervals.go index 0252302..e040b02 100644 --- a/pkg/database/partman/intervals.go +++ b/pkg/database/partman/intervals.go @@ -3,66 +3,23 @@ package partman import ( "fmt" "time" + + "dynatron.me/x/stillbox/internal/common" ) -const ( - daysInWeek = 7 - monthsInQuarter = 3 -) - -func getDailyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(0, 0, 1) - - return -} - -func getWeeklyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -int(date.Weekday()-time.Monday)) - upperBound = lowerBound.AddDate(0, 0, daysInWeek) - - return -} - -func getMonthlyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(0, 1, 0) - - return -} - -func getQuarterlyBounds(date time.Time) (lowerBound, upperBound time.Time) { - year, _, _ := date.Date() - - quarter := (int(date.Month()) - 1) / monthsInQuarter - firstMonthOfTheQuarter := time.Month(quarter*monthsInQuarter + 1) - - lowerBound = time.Date(year, firstMonthOfTheQuarter, 1, 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(0, monthsInQuarter, 0) - - return -} - -func getYearlyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), 1, 1, 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(1, 0, 0) - - return -} - func (p Partition) Next(i int) Partition { var t time.Time switch p.Interval { case Daily: t = p.Time.AddDate(0, 0, i) case Weekly: - t = p.Time.AddDate(0, 0, i*daysInWeek) + t = p.Time.AddDate(0, 0, i*common.DaysInWeek) case Monthly: year, month, _ := p.Time.Date() t = time.Date(year, month+time.Month(i), 1, 0, 0, 0, 0, p.Time.Location()) case Quarterly: - t = p.Time.AddDate(0, i*monthsInQuarter, 0) + t = p.Time.AddDate(0, i*common.MonthsInQuarter, 0) case Yearly: year, _, _ := p.Time.Date() @@ -125,13 +82,13 @@ func (p Partition) Prev(i int) Partition { case Daily: t = p.Time.AddDate(0, 0, -i) case Weekly: - t = p.Time.AddDate(0, 0, -i*daysInWeek) + t = p.Time.AddDate(0, 0, -i*common.DaysInWeek) case Monthly: year, month, _ := p.Time.Date() t = time.Date(year, month-time.Month(i), 1, 0, 0, 0, 0, p.Time.Location()) case Quarterly: - t = p.Time.AddDate(0, -i*monthsInQuarter, 0) + t = p.Time.AddDate(0, -i*common.MonthsInQuarter, 0) case Yearly: year, _, _ := p.Time.Date() @@ -150,3 +107,21 @@ func (p Partition) Prev(i int) Partition { return pp } + +func (p Partition) Range() (time.Time, time.Time) { + b := common.NewTimeBounder() + switch p.Interval { + case Daily: + return b.GetDailyBounds(p.Time) + case Weekly: + return b.GetWeeklyBounds(p.Time) + case Monthly: + return b.GetMonthlyBounds(p.Time) + case Quarterly: + return b.GetQuarterlyBounds(p.Time) + case Yearly: + return b.GetYearlyBounds(p.Time) + } + + panic("unknown interval!") +} diff --git a/pkg/database/partman/partman.go b/pkg/database/partman/partman.go index 9b7afcd..461d67b 100644 --- a/pkg/database/partman/partman.go +++ b/pkg/database/partman/partman.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/isoweek" "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" @@ -325,23 +326,6 @@ func (pm *partman) Check(ctx context.Context, now time.Time) error { }, pgx.TxOptions{}) } -func (p Partition) Range() (time.Time, time.Time) { - switch p.Interval { - case Daily: - return getDailyBounds(p.Time) - case Weekly: - return getWeeklyBounds(p.Time) - case Monthly: - return getMonthlyBounds(p.Time) - case Quarterly: - return getQuarterlyBounds(p.Time) - case Yearly: - return getYearlyBounds(p.Time) - } - - panic("unknown interval!") -} - func (p Partition) PartitionName() string { return p.Name } @@ -423,7 +407,7 @@ func (pm *partman) verifyPartName(pr database.PartitionResult) (p Partition, err if quarterNum > 4 { return p, PartitionError(pn, errors.New("invalid quarter")) } - firstMonthOfTheQuarter := time.Month((quarterNum-1)*monthsInQuarter + 1) + firstMonthOfTheQuarter := time.Month((quarterNum-1)*common.MonthsInQuarter + 1) parsed := time.Date(year, firstMonthOfTheQuarter, 1, 0, 0, 0, 0, time.UTC) if parsed != p.Time { return p, PartitionError(pn, ParsedIntvlErr{parsed: parsed, start: p.Time}) diff --git a/pkg/database/stats.sql.go b/pkg/database/stats.sql.go index 3db6ae8..6a54683 100644 --- a/pkg/database/stats.sql.go +++ b/pkg/database/stats.sql.go @@ -20,7 +20,7 @@ WHERE CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= $2 ELSE TRUE END AND CASE WHEN $3::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= $3 ELSE TRUE END + c.call_date < $3 ELSE TRUE END GROUP BY date ORDER BY date DESC ` @@ -61,7 +61,7 @@ WHERE CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= $2 ELSE TRUE END AND CASE WHEN $3::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= $3 ELSE TRUE END + c.call_date < $3 ELSE TRUE END GROUP BY 2, 3, 4 ORDER BY 4 DESC ` diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index 36a018a..5b2dc40 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -229,8 +229,13 @@ func (ia *incidentsAPI) getCallsM3U(id ID, w http.ResponseWriter, r *http.Reques return } var from string - if c.Source != 0 { + switch { + case c.Source != 0 && c.TalkerAlias != nil: + from = fmt.Sprintf(" from %s (%d)", *c.TalkerAlias, c.Source) + case c.Source != 0: from = fmt.Sprintf(" from %d", c.Source) + case c.TalkerAlias != nil: + from = fmt.Sprintf(" from %s", *c.TalkerAlias) } callUrl.Path = urlRoot + c.ID.String() diff --git a/pkg/stats/stats.go b/pkg/stats/stats.go index b1e03bd..a82991c 100644 --- a/pkg/stats/stats.go +++ b/pkg/stats/stats.go @@ -5,6 +5,7 @@ import ( "time" "dynatron.me/x/stillbox/internal/cache" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls/callstore" @@ -56,20 +57,25 @@ func (s *stats) GetCallStats(ctx context.Context, interval calls.StatsInterval) var start time.Time now := time.Now() + end := now + bnd := common.NewTimeBounder(common.WithLocation(now.Location())) + switch interval { case calls.IntervalHour: start = now.Add(-24 * time.Hour) // one day case calls.IntervalDay: start = now.Add(-7 * 24 * time.Hour) // one week case calls.IntervalWeek: - start = now.Add(-30 * 24 * time.Hour) // one month + start, end = bnd.GetMonthlyBounds(now) + start, _ = bnd.GetWeeklyBounds(start) + _, end = bnd.GetWeeklyBounds(end) case calls.IntervalMonth: start = now.Add(-365 * 24 * time.Hour) // one year default: return nil, calls.ErrInvalidInterval } - st, err := s.cs.CallStats(ctx, interval, jsontypes.Time(start), jsontypes.Time(now)) + st, err := s.cs.CallStats(ctx, interval, jsontypes.Time(start), jsontypes.Time(end)) if err != nil { return nil, err } diff --git a/sql/postgres/queries/calls.sql b/sql/postgres/queries/calls.sql index 0898863..13798c4 100644 --- a/sql/postgres/queries/calls.sql +++ b/sql/postgres/queries/calls.sql @@ -127,6 +127,9 @@ CASE WHEN sqlc.narg('tags_not')::TEXT[] IS NOT NULL THEN ) ELSE TRUE END) AND (CASE WHEN sqlc.narg('longer_than')::NUMERIC IS NOT NULL THEN ( c.duration > @longer_than + ) ELSE TRUE END) AND +(CASE WHEN @unknown_tg::BOOLEAN = TRUE THEN ( + tgs.tgid IS NULL ) ELSE TRUE END) GROUP BY c.id, c.call_date ORDER BY @@ -157,6 +160,9 @@ CASE WHEN sqlc.narg('tags_not')::TEXT[] IS NOT NULL THEN ) ELSE TRUE END) AND (CASE WHEN sqlc.narg('longer_than')::NUMERIC IS NOT NULL THEN ( c.duration > @longer_than + ) ELSE TRUE END) AND +(CASE WHEN @unknown_tg::BOOLEAN = TRUE THEN ( + tgs.tgid IS NULL ) ELSE TRUE END) ; diff --git a/sql/postgres/queries/stats.sql b/sql/postgres/queries/stats.sql index e201da8..32538c7 100644 --- a/sql/postgres/queries/stats.sql +++ b/sql/postgres/queries/stats.sql @@ -9,7 +9,7 @@ WHERE CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= @start ELSE TRUE END AND CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= sqlc.narg('end') ELSE TRUE END + c.call_date < sqlc.narg('end') ELSE TRUE END GROUP BY 2, 3, 4 ORDER BY 4 DESC; @@ -22,6 +22,6 @@ WHERE CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= @start ELSE TRUE END AND CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= sqlc.narg('end') ELSE TRUE END + c.call_date < sqlc.narg('end') ELSE TRUE END GROUP BY date ORDER BY date DESC;