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
-
+
+
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;
|