Merge pull request 'Use Partman intervals, chart improvements' (#121) from partmanIntvl115 into trunk
Closes #115 Reviewed-on: #121
This commit is contained in:
commit
8fd0c85c1e
21 changed files with 331 additions and 156 deletions
|
@ -4,6 +4,7 @@ export interface CallRecord {
|
|||
audioURL: string | null;
|
||||
duration: number;
|
||||
systemId: number;
|
||||
talkerAlias: string | null;
|
||||
tgid: number;
|
||||
incidents: number; // in incident
|
||||
}
|
||||
|
|
|
@ -168,6 +168,10 @@
|
|||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="talker">
|
||||
<th mat-header-cell *matHeaderCellDef>Source</th>
|
||||
<td mat-cell *matCellDef="let call" [innerHTML]="call | talker"></td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="duration">
|
||||
<th mat-header-cell *matHeaderCellDef class="durationHdr">Duration</th>
|
||||
<td mat-cell *matCellDef="let call" class="duration">
|
||||
|
|
|
@ -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 = <PageEvent>{ pageIndex: 0, pageSize: 0 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
<div class="chart"></div>
|
||||
<div class="spinner" *ngIf="loading"><mat-spinner></mat-spinner></div>
|
||||
|
|
|
@ -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<string>;
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
<mat-card class="chart" appearance="outlined">
|
||||
<div class="chartTitle">Calls By Week</div>
|
||||
<chart interval="week"></chart>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field>
|
||||
<label>Calls By</label>
|
||||
<mat-select formControlName="callsPer">
|
||||
<mat-option value="hour">Hour</mat-option>
|
||||
<mat-option value="day">Day</mat-option>
|
||||
<mat-option value="week">Week</mat-option>
|
||||
<mat-option value="month">Month</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<chart #chart [interval]="callsOb"></chart>
|
||||
</mat-card>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
mat-card.chart {
|
||||
width: 500px;
|
||||
height: 400px;
|
||||
height: 500px;
|
||||
margin: 30px 30px 40px 40px;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string>;
|
||||
|
||||
ngOnInit() {
|
||||
this.form.controls['callsPer'].setValue('week');
|
||||
this.callsOb = this.form.controls['callsPer'].valueChanges.pipe(
|
||||
startWith('week'),
|
||||
debounceTime(300),
|
||||
filter((val) => val !== null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
84
internal/common/intervals.go
Normal file
84
internal/common/intervals.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!")
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue