Compare commits
3 commits
main
...
safe_waiti
| Author | SHA1 | Date | |
|---|---|---|---|
| f99550d879 | |||
|
|
bedd062aa0 | ||
| 736e1f4d39 |
8 changed files with 145 additions and 28 deletions
|
|
@ -41,7 +41,7 @@ export class NtfyClient {
|
||||||
throw new Error(`${response.status} - ${response.statusText}`)
|
throw new Error(`${response.status} - ${response.statusText}`)
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.loggerService.error('ntfy client failed', { error: error instanceof Error ? error.message : JSON.stringify(error) })
|
this.loggerService.error('ntfy client failed', { error })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ export class NtfyProvider implements OnApplicationBootstrap {
|
||||||
async sendBootstrappedNotification() {
|
async sendBootstrappedNotification() {
|
||||||
const version =
|
const version =
|
||||||
this.configService.get<string>('GIT_COMMIT') ??
|
this.configService.get<string>('GIT_COMMIT') ??
|
||||||
(this.configService.get('LOCAL') === 'true'
|
this.configService.get('LOCAL') === 'true'
|
||||||
? 'LOCAL'
|
? 'LOCAL'
|
||||||
: 'unknown')
|
: 'unknown'
|
||||||
await this.publishQueue.add(
|
await this.publishQueue.add(
|
||||||
...NtfyProvider.defaultJob({
|
...NtfyProvider.defaultJob({
|
||||||
title: 'Autobaan up and running',
|
title: 'Autobaan up and running',
|
||||||
|
|
@ -107,6 +107,39 @@ export class NtfyProvider implements OnApplicationBootstrap {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendPerformingRiskyReservationNotification(
|
||||||
|
reservationId: string,
|
||||||
|
startTime: Dayjs,
|
||||||
|
endTime: Dayjs,
|
||||||
|
) {
|
||||||
|
const url = `${this.configService.get(
|
||||||
|
'BASE_URL',
|
||||||
|
)}/reservations/${reservationId}`
|
||||||
|
await this.publishQueue.add(
|
||||||
|
...NtfyProvider.defaultJob({
|
||||||
|
title: 'Handling risky reservation. Waiting for confirmation',
|
||||||
|
message: `Waiting for ${reservationId} - ${startTime.format()} to ${endTime.format()}`,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'http',
|
||||||
|
label: 'Accept',
|
||||||
|
url: `${url}/resume`,
|
||||||
|
method: 'POST',
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'http',
|
||||||
|
label: 'Reject',
|
||||||
|
url,
|
||||||
|
method: 'DELETE',
|
||||||
|
clear: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: [MessageTags.warning, MessageTags.passport_control],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async sendErrorPerformingReservationNotification(
|
async sendErrorPerformingReservationNotification(
|
||||||
reservationId: string,
|
reservationId: string,
|
||||||
startTime: Dayjs,
|
startTime: Dayjs,
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export enum MessageTags {
|
||||||
clock11 = 'clock11',
|
clock11 = 'clock11',
|
||||||
clock1130 = 'clock1130',
|
clock1130 = 'clock1130',
|
||||||
badminton = 'badminton',
|
badminton = 'badminton',
|
||||||
|
passport_control = 'passport_control',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MessagePriority {
|
export enum MessagePriority {
|
||||||
|
|
@ -59,13 +60,60 @@ export enum MessagePriority {
|
||||||
max = 5,
|
max = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageActionType = 'broadcast' | 'copy' | 'http' | 'view'
|
||||||
|
|
||||||
|
export interface MessageActionConfig {
|
||||||
|
action: MessageActionType
|
||||||
|
/**
|
||||||
|
* Label displayed on the action button
|
||||||
|
*/
|
||||||
|
label: string
|
||||||
|
/**
|
||||||
|
* Clear notification after action button is tapped
|
||||||
|
*/
|
||||||
|
clear?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BroadcastActionConfig extends MessageActionConfig {
|
||||||
|
action: 'broadcast'
|
||||||
|
intent?: string
|
||||||
|
extras?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopyActionConfig extends MessageActionConfig {
|
||||||
|
action: 'copy'
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpActionConfig extends MessageActionConfig {
|
||||||
|
action: 'http'
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* @default 'POST'
|
||||||
|
*/
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' // there are more but if I use them I'll have gone insane
|
||||||
|
headers?: Record<string, string>
|
||||||
|
body?: string
|
||||||
|
}
|
||||||
|
export interface ViewActionConfig extends MessageActionConfig {
|
||||||
|
action: 'view'
|
||||||
|
url: string
|
||||||
|
clear?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageAction =
|
||||||
|
| BroadcastActionConfig
|
||||||
|
| CopyActionConfig
|
||||||
|
| HttpActionConfig
|
||||||
|
| ViewActionConfig
|
||||||
|
|
||||||
export interface MessageConfig {
|
export interface MessageConfig {
|
||||||
topic: string
|
topic: string
|
||||||
message?: string
|
message?: string
|
||||||
title?: string
|
title?: string
|
||||||
tags?: MessageTags[]
|
tags?: MessageTags[]
|
||||||
priority?: MessagePriority
|
priority?: MessagePriority
|
||||||
actions?: object[]
|
actions?: MessageAction[]
|
||||||
markdown?: boolean
|
markdown?: boolean
|
||||||
icon?: string
|
icon?: string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import { Transform } from 'class-transformer'
|
||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDateString,
|
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
|
@ -58,12 +57,10 @@ export class CreateReservationRequest {
|
||||||
readonly ownerId: string
|
readonly ownerId: string
|
||||||
|
|
||||||
@Transform(DayjsTransformer)
|
@Transform(DayjsTransformer)
|
||||||
@IsDateString()
|
|
||||||
readonly dateRangeStart: Dayjs
|
readonly dateRangeStart: Dayjs
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(DayjsTransformer)
|
@Transform(DayjsTransformer)
|
||||||
@IsDateString()
|
|
||||||
readonly dateRangeEnd?: Dayjs
|
readonly dateRangeEnd?: Dayjs
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,7 @@ export class ReservationsCronService {
|
||||||
await this.ntfyProvider.sendCronStartNotification(
|
await this.ntfyProvider.sendCronStartNotification(
|
||||||
'cleanUpExpiredReservations',
|
'cleanUpExpiredReservations',
|
||||||
)
|
)
|
||||||
const reservations = await this.reservationService.getOlderThanDate(
|
const reservations = await this.reservationService.getByDate()
|
||||||
dayjs().subtract(7, 'day'),
|
|
||||||
)
|
|
||||||
this.loggerService.debug(
|
this.loggerService.debug(
|
||||||
`Found ${reservations.length} reservations to delete`,
|
`Found ${reservations.length} reservations to delete`,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ import { LoggerService } from '../logger/service.logger'
|
||||||
import { BaanReserverenService } from '../runner/baanreserveren/service'
|
import { BaanReserverenService } from '../runner/baanreserveren/service'
|
||||||
import { Reservation, ReservationStatus } from './entity'
|
import { Reservation, ReservationStatus } from './entity'
|
||||||
|
|
||||||
|
export enum ReservationDangerLevel {
|
||||||
|
Safe = 'safe',
|
||||||
|
Risky = 'risky',
|
||||||
|
Lethal = 'lethal',
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReservationsService {
|
export class ReservationsService {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -41,17 +47,6 @@ export class ReservationsService {
|
||||||
return await qb.orderBy('dateRangeStart', 'ASC').getMany()
|
return await qb.orderBy('dateRangeStart', 'ASC').getMany()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOlderThanDate(date = dayjs()) {
|
|
||||||
const query = this.reservationsRepository
|
|
||||||
.createQueryBuilder()
|
|
||||||
.where(`(DATE(dateRangeStart) < DATE(:startDate)`, {
|
|
||||||
startDate: date.toISOString(),
|
|
||||||
})
|
|
||||||
.orderBy('dateRangeStart', 'ASC')
|
|
||||||
|
|
||||||
return await query.getMany()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all reservations that have not been scheduled that are within the reservation window
|
* Gets all reservations that have not been scheduled that are within the reservation window
|
||||||
* @returns Reservations that can be scheduled
|
* @returns Reservations that can be scheduled
|
||||||
|
|
@ -104,6 +99,19 @@ export class ReservationsService {
|
||||||
return await this.reservationsRepository.save(res)
|
return await this.reservationsRepository.save(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDangerLevel(reservation: Reservation) {
|
||||||
|
// don't book something within the ~~danger zone~~
|
||||||
|
const now = dayjs()
|
||||||
|
const hourDiff = now.diff(reservation.dateRangeStart, 'hours')
|
||||||
|
if (hourDiff > 8) {
|
||||||
|
return ReservationDangerLevel.Safe
|
||||||
|
} else if (hourDiff >= 5) {
|
||||||
|
return ReservationDangerLevel.Risky
|
||||||
|
} else {
|
||||||
|
return ReservationDangerLevel.Lethal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async update(reservationId: string, update: Partial<Reservation>) {
|
async update(reservationId: string, update: Partial<Reservation>) {
|
||||||
return await this.reservationsRepository.update(reservationId, update)
|
return await this.reservationsRepository.update(reservationId, update)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -912,12 +912,8 @@ export class BaanReserverenService {
|
||||||
try {
|
try {
|
||||||
currentAttempt++
|
currentAttempt++
|
||||||
await this.init()
|
await this.init()
|
||||||
break
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof RunningReservationsNavigationError) {
|
if (err instanceof RunningReservationsNavigationError) {
|
||||||
this.loggerService.warn('Hit expected warmup error, retrying', {
|
|
||||||
currentAttempt,
|
|
||||||
})
|
|
||||||
await new Promise((res) => setTimeout(res, delay))
|
await new Promise((res) => setTimeout(res, delay))
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ import {
|
||||||
RESERVATIONS_QUEUE_NAME,
|
RESERVATIONS_QUEUE_NAME,
|
||||||
ReservationsQueue,
|
ReservationsQueue,
|
||||||
} from '../reservations/config'
|
} from '../reservations/config'
|
||||||
import { ReservationsService } from '../reservations/service'
|
import { Reservation } from '../reservations/entity'
|
||||||
|
import {
|
||||||
|
ReservationDangerLevel,
|
||||||
|
ReservationsService,
|
||||||
|
} from '../reservations/service'
|
||||||
import { WaitingListDetails } from './types'
|
import { WaitingListDetails } from './types'
|
||||||
|
|
||||||
const EMAIL_SUBJECT_REGEX = new RegExp(
|
const EMAIL_SUBJECT_REGEX = new RegExp(
|
||||||
|
|
@ -90,12 +94,45 @@ export class WaitingListService {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't book something within the ~~danger zone~~
|
||||||
|
const partitioned = reservations.reduce<{
|
||||||
|
safe: Reservation[]
|
||||||
|
risky: Reservation[]
|
||||||
|
lethal: Reservation[]
|
||||||
|
}>(
|
||||||
|
(acc, res) => {
|
||||||
|
switch (this.reservationsService.getDangerLevel(res)) {
|
||||||
|
case ReservationDangerLevel.Safe:
|
||||||
|
return {
|
||||||
|
safe: [...acc.safe, res],
|
||||||
|
risky: acc.risky,
|
||||||
|
lethal: acc.lethal,
|
||||||
|
}
|
||||||
|
case ReservationDangerLevel.Risky:
|
||||||
|
return {
|
||||||
|
safe: acc.safe,
|
||||||
|
risky: [...acc.risky, res],
|
||||||
|
lethal: acc.lethal,
|
||||||
|
}
|
||||||
|
case ReservationDangerLevel.Lethal:
|
||||||
|
return {
|
||||||
|
safe: acc.safe,
|
||||||
|
risky: acc.risky,
|
||||||
|
lethal: [...acc.lethal, res],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ safe: [], risky: [], lethal: [] },
|
||||||
|
)
|
||||||
|
|
||||||
this.loggerService.debug(
|
this.loggerService.debug(
|
||||||
`Found ${reservations.length} reservations on waiting list`,
|
`Found the reservations in given categories: ${JSON.stringify(
|
||||||
|
partitioned,
|
||||||
|
)}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.reservationsQueue.addBulk(
|
await this.reservationsQueue.addBulk(
|
||||||
reservations.map((r) => ({
|
partitioned.safe.map((r) => ({
|
||||||
data: { reservation: r, speedyMode: false },
|
data: { reservation: r, speedyMode: false },
|
||||||
opts: { attempts: 1 },
|
opts: { attempts: 1 },
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue