Compare commits

..

26 commits

Author SHA1 Message Date
Collin Duncan
ed05a330f6
updating deploy to include GIT_COMMIT as env var
All checks were successful
ci/woodpecker/push/test-and-deploy Pipeline was successful
2025-04-29 10:46:23 +02:00
Collin Duncan
543a8c4ada
adding ecosystem config file to post-deploy step
All checks were successful
ci/woodpecker/push/test-and-deploy Pipeline was successful
2025-04-29 10:22:23 +02:00
Collin Duncan
be0a32c8eb
using name argument
Some checks failed
ci/woodpecker/push/test-and-deploy Pipeline failed
2025-04-29 09:56:19 +02:00
Collin Duncan
21495eea58
Changing where the deploy action comes from
Some checks failed
ci/woodpecker/push/test-and-deploy Pipeline failed
2025-04-29 09:41:23 +02:00
Collin Duncan
42da932301
changing start to startOrRestart
Some checks failed
ci/woodpecker/push/test-and-deploy Pipeline failed
2025-04-29 09:15:28 +02:00
a78444749c Updating deploy script and cicd name
Some checks failed
ci/woodpecker/push/test-and-deploy Pipeline failed
2025-04-28 21:45:07 +02:00
Collin Duncan
d84c23bdeb
changing host back to autobaan
All checks were successful
ci/woodpecker/push/test Pipeline was successful
2025-04-28 17:12:04 +02:00
Collin Duncan
14ba6363ee
tmp
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-28 17:09:24 +02:00
Collin Duncan
8863a9d052
adding origin to ref
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-28 16:48:31 +02:00
Collin Duncan
430f73d63b
Testing correct ref and removing old ssh check
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-28 16:30:08 +02:00
Collin Duncan
b1558b518a
using non-slim image
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-28 16:13:29 +02:00
Collin Duncan
691deb7765
adding deploy step
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-28 13:58:51 +02:00
Collin Duncan
93bcc887b9
updating pm2 ecosystem config
All checks were successful
ci/woodpecker/push/test Pipeline was successful
2025-04-28 13:54:37 +02:00
Collin Duncan
0ac1644eb1
Adding pm2 ecosystem and deploy config
All checks were successful
ci/woodpecker/push/test Pipeline was successful
2025-04-28 13:45:11 +02:00
Collin Duncan
eff0e5849e
using config on server for ssh known hosts
All checks were successful
ci/woodpecker/push/test Pipeline was successful
2025-04-24 14:39:57 +02:00
Collin Duncan
7bc95a8217
testing network addresses
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-24 14:24:28 +02:00
Collin Duncan
2fb73310b1
testing some bullshit
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-24 14:22:06 +02:00
Collin Duncan
e2b420ef56
changing to use volumes instead of secrets
Some checks failed
ci/woodpecker/manual/test Pipeline failed
2025-04-24 13:47:36 +02:00
Collin Duncan
e3c91dd5c6
fixing typo in manual trigger
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-24 13:33:20 +02:00
Collin Duncan
05f63fe6c2
adding manual trigger 2025-04-24 13:32:54 +02:00
Collin Duncan
aaf9cfde1f
getting ssh key from secrets 2025-04-24 13:31:04 +02:00
Collin Duncan
3ca68c5841
changing image for deploy step
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-24 13:12:55 +02:00
Collin Duncan
78b2334e4b
Removing branch
Some checks failed
ci/woodpecker/push/test Pipeline failed
2025-04-24 11:52:29 +02:00
Collin Duncan
1945b0dd40
Updating branch to all branches 2025-04-24 11:51:55 +02:00
Collin Duncan
cd613af872
Updating image 2025-04-24 11:47:01 +02:00
Collin Duncan
93ec1fb4db
Adding new step for woodpecker-cicd to ssh into autobaan container 2025-04-24 11:46:08 +02:00
12 changed files with 75 additions and 232 deletions

43
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Push to main
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
- run: npm ci
- run: npm run test:unit
build-image:
needs:
- test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_PASSWORD }}
- name: Declare GIT_COMMIT
shell: bash
run: |
echo "GIT_COMMIT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV"
- name: Build docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/server/Dockerfile
push: true
tags: ghcr.io/cgduncan7/autobaan:latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: GIT_COMMIT=${{ env.GIT_COMMIT }}

View file

@ -1,19 +1,13 @@
when: when:
- event: push - event: push
branch: main
- event: manual - event: manual
branch: main
steps: steps:
- name: deploy - name: test-and-deploy
image: docker.io/node:hydrogen image: docker.io/node:hydrogen
volumes: volumes:
- /etc/ssh:/etc/ssh - /etc/ssh:/etc/ssh
commands: commands:
- npm ci - npm ci
- npm run test:unit
- npm run deploy - npm run deploy
depends_on:
- test
runs_on: [success]

View file

@ -1,12 +0,0 @@
when:
- event: pull_request
- event: push
branch: main
- event: manual
steps:
- name: test
image: docker.io/node:hydrogen
commands:
- npm ci
- npm run test:unit

View file

@ -1,7 +1,5 @@
# autobaan # autobaan
![alt text](https://woody.collinduncan.com/api/badges/1/status.svg)
Automatic court reservation! Automatic court reservation!
## Setup ## Setup
@ -10,7 +8,7 @@ Automatic court reservation!
- Node.js (18.x) - Node.js (18.x)
- npm (8.x) - npm (8.x)
- nvm - nvm
- Docker - Docker
- redis - redis
@ -35,4 +33,4 @@ npm start:dev
### CD ### CD
Using woodpecker to deploy this via pm2. So I don't forget... I am using GHA to create a container image which I pull on my server using podman. This then restarts the container on my server with the latest image. The container is backed by a systemd service to restart and start on boot.

View file

@ -1,33 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class ReservationCancellation1774364085498
implements MigrationInterface
{
name = 'ReservationCancellation1774364085498'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "status" varchar(32) NOT NULL DEFAULT ('pending'), "waitingListId" integer, "ownerId" varchar(32) NOT NULL, "opponents" json NOT NULL, "externalId" varchar(32), "bookingCourtNumber" varchar(32), "bookingStartAt" varchar(32))`,
)
await queryRunner.query(
`INSERT INTO "temporary_reservations"("id", "dateRangeStart", "dateRangeEnd", "status", "waitingListId", "ownerId", "opponents") SELECT "id", "dateRangeStart", "dateRangeEnd", "status", "waitingListId", "ownerId", "opponents" FROM "reservations"`,
)
await queryRunner.query(`DROP TABLE "reservations"`)
await queryRunner.query(
`ALTER TABLE "temporary_reservations" RENAME TO "reservations"`,
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "reservations" RENAME TO "temporary_reservations"`,
)
await queryRunner.query(
`CREATE TABLE "reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "status" varchar(32) NOT NULL DEFAULT ('pending'), "waitingListId" integer, "ownerId" varchar(32) NOT NULL, "opponents" json NOT NULL)`,
)
await queryRunner.query(
`INSERT INTO "reservations"("id", "dateRangeStart", "dateRangeEnd", "status", "waitingListId", "ownerId", "opponents") SELECT "id", "dateRangeStart", "dateRangeEnd", "status", "waitingListId", "ownerId", "opponents" FROM "temporary_reservations"`,
)
await queryRunner.query(`DROP TABLE "temporary_reservations"`)
}
}

View file

@ -9,8 +9,8 @@ module.exports = {
deploy: { deploy: {
production: { production: {
user: 'root', user: 'root',
host: ['autobaan.home'], host: ['autobaan'],
ref: 'origin/main', ref: 'origin/deploy_test',
repo: 'https://fred.collinduncan.com/collin/autobaan.git', repo: 'https://fred.collinduncan.com/collin/autobaan.git',
path: '/root/autobaan', path: '/root/autobaan',
'post-deploy': 'post-deploy':

View file

@ -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 })
} }
} }
} }

View file

@ -60,15 +60,11 @@ export class NtfyProvider implements OnApplicationBootstrap {
} }
async sendBootstrappedNotification() { async sendBootstrappedNotification() {
const version = const gitCommit = this.configService.get<string>('GIT_COMMIT')
this.configService.get<string>('GIT_COMMIT') ??
this.configService.get('LOCAL') === 'true'
? 'LOCAL'
: 'unknown'
await this.publishQueue.add( await this.publishQueue.add(
...NtfyProvider.defaultJob({ ...NtfyProvider.defaultJob({
title: 'Autobaan up and running', title: 'Autobaan up and running',
message: `Version ${version}`, message: `Version ${gitCommit}`,
tags: [MessageTags.badminton], tags: [MessageTags.badminton],
}), }),
) )

View file

@ -24,9 +24,6 @@ export class Reservation {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string id: string
@Column('varchar', { length: 32, nullable: true })
externalId?: string
@Column('varchar', { length: 32, nullable: false }) @Column('varchar', { length: 32, nullable: false })
ownerId: string ownerId: string
@ -60,18 +57,6 @@ export class Reservation {
@Column('int', { nullable: true }) @Column('int', { nullable: true })
waitingListId: number waitingListId: number
@Column('varchar', {
length: 32,
nullable: true,
})
bookingCourtNumber?: string
@Column('varchar', {
length: 32,
nullable: true,
})
bookingStartAt?: string
constructor(partial: Partial<Reservation>) { constructor(partial: Partial<Reservation>) {
Object.assign(this, partial) Object.assign(this, partial)
} }

View file

@ -101,10 +101,7 @@ export class ReservationsService {
const reservation = await this.getById(id) const reservation = await this.getById(id)
if (!reservation) return if (!reservation) return
if (reservation.status === ReservationStatus.Booked) await this.brService.removeReservationFromWaitList(reservation)
await this.brService.cancelReservation(reservation)
else if (reservation.status === ReservationStatus.OnWaitingList)
await this.brService.removeReservationFromWaitList(reservation)
return await this.reservationsRepository.delete({ id: reservation.id }) return await this.reservationsRepository.delete({ id: reservation.id })
} }
} }

View file

@ -83,33 +83,13 @@ export class ReservationsWorker {
speedyMode = true, speedyMode = true,
) { ) {
try { try {
let externalId: string
let startAt: string
let court: number
if (speedyMode) { if (speedyMode) {
const { await this.brService.performSpeedyReservation(reservation)
id,
startAt: _startAt,
court: _court,
} = await this.brService.performSpeedyReservation(reservation)
externalId = id
startAt = _startAt
court = _court
} else { } else {
const { await this.brService.performReservation(reservation, timeSensitive)
id,
startAt: _startAt,
court: _court,
} = await this.brService.performReservation(reservation, timeSensitive)
externalId = id
startAt = _startAt
court = _court
} }
await this.reservationsService.update(reservation.id, { await this.reservationsService.update(reservation.id, {
status: ReservationStatus.Booked, status: ReservationStatus.Booked,
externalId: externalId == null ? undefined : `${externalId}`,
bookingCourtNumber: `${court}`,
bookingStartAt: startAt,
}) })
} catch (error: unknown) { } catch (error: unknown) {
await this.handleReservationErrors( await this.handleReservationErrors(

View file

@ -23,7 +23,6 @@ export enum BaanReserverenUrls {
Logout = '/auth/logout', Logout = '/auth/logout',
WaitingList = '/waitinglist', WaitingList = '/waitinglist',
WaitingListAdd = '/waitinglist/add', WaitingListAdd = '/waitinglist/add',
FutureReservations = '/user/future',
} }
enum SessionAction { enum SessionAction {
@ -75,9 +74,9 @@ const CourtRank: Record<CourtSlot, number> = {
[CourtSlot.One]: 0, [CourtSlot.One]: 0,
[CourtSlot.Two]: 2, // team at squash city has this pre-booked at 19.15 on Tuesday :sad: [CourtSlot.Two]: 2, // team at squash city has this pre-booked at 19.15 on Tuesday :sad:
[CourtSlot.Three]: 2, // team at squash city has this pre-booked at 19.15 on Tuesday :sad: [CourtSlot.Three]: 2, // team at squash city has this pre-booked at 19.15 on Tuesday :sad:
[CourtSlot.Four]: 1, // lower tin [CourtSlot.Four]: 0,
[CourtSlot.Five]: 9, // Из говна и палок (made from shit and sticks) [CourtSlot.Five]: 0,
[CourtSlot.Six]: 9, // Держится на соплях (hanging on by a thread) [CourtSlot.Six]: 0,
[CourtSlot.Seven]: 0, [CourtSlot.Seven]: 0,
[CourtSlot.Eight]: 0, [CourtSlot.Eight]: 0,
[CourtSlot.Nine]: 0, [CourtSlot.Nine]: 0,
@ -341,17 +340,6 @@ export class BaanReserverenService {
return lastDayOfMonth.add(daysToAdd, 'day') return lastDayOfMonth.add(daysToAdd, 'day')
} }
private async navigateToReservationCancellation(
reservation: Reservation & Required<Pick<Reservation, 'externalId'>>,
) {
this.loggerService.debug('Navigating to reservation', {
externalId: reservation.externalId,
})
await this.page.goto(
`${BAAN_RESERVEREN_ROOT_URL}/reservations/${reservation.externalId}/cancel`,
)
}
private async navigateToReservationPrompt(courtSlot: CourtSlot, date: Dayjs) { private async navigateToReservationPrompt(courtSlot: CourtSlot, date: Dayjs) {
this.loggerService.debug('Navigating to reservation prompt', { this.loggerService.debug('Navigating to reservation prompt', {
courtSlot, courtSlot,
@ -426,22 +414,6 @@ export class BaanReserverenService {
await this.page.waitForNetworkIdle() await this.page.waitForNetworkIdle()
} }
private findFutureReservationLink(): string {
return ''
}
private async navigateToFutureReservations() {
this.loggerService.debug('Navigating to future reservations')
await this.page
.goto(
`${BAAN_RESERVEREN_ROOT_URL}/${BaanReserverenUrls.FutureReservations}`,
)
.catch((e) => {
throw new RunningFutureReservationsNavigationError(e)
})
await this.page.waitForNetworkIdle()
}
private async recordWaitingListEntries(): Promise<number[]> { private async recordWaitingListEntries(): Promise<number[]> {
const waitingListEntriesElements = await this.page.$$( const waitingListEntriesElements = await this.page.$$(
'#content tbody tr td:nth-child(1)', '#content tbody tr td:nth-child(1)',
@ -505,13 +477,14 @@ export class BaanReserverenService {
await this.page.waitForNetworkIdle() await this.page.waitForNetworkIdle()
} }
// As a wise man once said, «Из говна и палок»
private async sortCourtsByRank( private async sortCourtsByRank(
freeCourts: ElementHandle<Element>[], freeCourts: ElementHandle<Element>[],
): Promise<{ handle: ElementHandle; courtSlot: CourtSlot } | null> { ): Promise<ElementHandle | null> {
if (freeCourts.length === 0) return null if (freeCourts.length === 0) return null
const freeCourtsWithJson = await Promise.all( const freeCourtsWithJson = await Promise.all(
freeCourts.map(async (fc) => ({ freeCourts.map(async (fc) => ({
elementHandle: fc, elementHAndle: fc,
jsonValue: await fc.jsonValue(), jsonValue: await fc.jsonValue(),
})), })),
) )
@ -520,46 +493,36 @@ export class BaanReserverenService {
(CourtRank[a.jsonValue.slot as CourtSlot] ?? 99) - (CourtRank[a.jsonValue.slot as CourtSlot] ?? 99) -
(CourtRank[b.jsonValue.slot as CourtSlot] ?? 99), (CourtRank[b.jsonValue.slot as CourtSlot] ?? 99),
) )
return { return freeCourtsWithJson[0].elementHAndle
handle: freeCourtsWithJson[0].elementHandle,
courtSlot: freeCourtsWithJson[0].jsonValue.slot as CourtSlot,
}
} }
private async selectAvailableTime(reservation: Reservation) { private async selectAvailableTime(reservation: Reservation) {
this.loggerService.debug('Selecting available time', { this.loggerService.debug('Selecting available time', {
reservation: instanceToPlain(reservation), reservation: instanceToPlain(reservation),
}) })
let freeCourtHandle: ElementHandle | null | undefined let freeCourt: ElementHandle | null | undefined
let freeCourtSlot: CourtSlot | undefined
let time: string | undefined
let i = 0 let i = 0
const possibleDates = reservation.createPossibleDates() const possibleDates = reservation.createPossibleDates()
this.loggerService.debug('Possible dates', { possibleDates }) this.loggerService.debug('Possible dates', { possibleDates })
while (i < possibleDates.length && !freeCourtHandle) { while (i < possibleDates.length && !freeCourt) {
const possibleDate = possibleDates[i] const possibleDate = possibleDates[i]
const timeString = possibleDate.format('HH:mm') const timeString = possibleDate.format('HH:mm')
const selector = const selector =
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']` `tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']`
const freeCourts = await this.page.$$(selector) const freeCourts = await this.page.$$(selector)
const firstFreeCourt = await this.sortCourtsByRank(freeCourts) freeCourt = await this.sortCourtsByRank(freeCourts)
freeCourtHandle = firstFreeCourt?.handle
freeCourtSlot = firstFreeCourt?.courtSlot
time = timeString
i++ i++
} }
if (!freeCourtHandle || !freeCourtSlot || !time) { if (!freeCourt) {
throw new NoCourtAvailableError('No court available for reservation') throw new NoCourtAvailableError('No court available for reservation')
} }
this.loggerService.debug('Free court found', { court: freeCourtSlot }) this.loggerService.debug('Free court found')
await freeCourtHandle.click().catch((e: Error) => { await freeCourt.click().catch((e: Error) => {
throw new RunnerCourtSelectionError(e) throw new RunnerCourtSelectionError(e)
}) })
return { time, court: CourtSlotToNumber[freeCourtSlot] }
} }
private async selectOwner(id: string) { private async selectOwner(id: string) {
@ -634,23 +597,6 @@ export class BaanReserverenService {
throw new RunnerReservationConfirmSubmitError(e) throw new RunnerReservationConfirmSubmitError(e)
}) })
await this.page.waitForNetworkIdle() await this.page.waitForNetworkIdle()
const regexResult = /reservations\/(\d+)$/.exec(this.page.url())
if (regexResult != null) {
return regexResult[1]
}
return 'unknown'
}
private async confirmCancellation() {
this.loggerService.debug('Cancelling reservation')
await this.page
.$('input[value="Reservering annuleren"]')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerReservationCancelButtonError(e)
})
} }
private async inputWaitingListDetails(reservation: Reservation) { private async inputWaitingListDetails(reservation: Reservation) {
@ -757,22 +703,17 @@ export class BaanReserverenService {
try { try {
await this.init() await this.init()
await this.navigateToDay(reservation.dateRangeStart) await this.navigateToDay(reservation.dateRangeStart)
const { time, court } = await this.selectAvailableTime(reservation) await this.selectAvailableTime(reservation)
await this.selectOwner(reservation.ownerId) await this.selectOwner(reservation.ownerId)
await this.selectOpponents(reservation.opponents) await this.selectOpponents(reservation.opponents)
const id = await this.confirmReservation() await this.confirmReservation()
return { id, startAt: time, court }
} catch (error: unknown) { } catch (error: unknown) {
if (!timeSensitive) await this.handleError() if (!timeSensitive) await this.handleError()
throw error throw error
} }
} }
public async performSpeedyReservation(reservation: Reservation): Promise<{ public async performSpeedyReservation(reservation: Reservation) {
id: string
startAt: string
court: number
}> {
await this.init() await this.init()
const courtSlots = this.getCourtSlotsForDate(reservation.dateRangeStart) const courtSlots = this.getCourtSlotsForDate(reservation.dateRangeStart)
@ -781,6 +722,7 @@ export class BaanReserverenService {
} }
for (const courtSlot of courtSlots) { for (const courtSlot of courtSlots) {
let errorReserving = false
try { try {
await this.navigateToReservationPrompt( await this.navigateToReservationPrompt(
courtSlot, courtSlot,
@ -788,51 +730,24 @@ export class BaanReserverenService {
) )
await this.selectOwner(reservation.ownerId) await this.selectOwner(reservation.ownerId)
await this.selectOpponents(reservation.opponents, true) await this.selectOpponents(reservation.opponents, true)
const id = await this.confirmReservation() await this.confirmReservation()
return {
id,
startAt: reservation.dateRangeStart.format('HH:mm'),
court: CourtSlotToNumber[courtSlot],
}
} catch (error: unknown) { } catch (error: unknown) {
if ( if (
error instanceof CourtNotAvailableError || error instanceof CourtNotAvailableError ||
error instanceof RunnerReservationConfirmSubmitError error instanceof RunnerReservationConfirmSubmitError
) { ) {
this.loggerService.warn('Court taken, retrying', { courtSlot }) this.loggerService.warn('Court taken, retrying', { courtSlot })
errorReserving = true
} else { } else {
throw error throw error
} }
} }
if (!errorReserving) return
} }
throw new NoCourtAvailableError('Could not reserve court') throw new NoCourtAvailableError('Could not reserve court')
} }
public async cancelReservation(reservation: Reservation) {
if (reservation.externalId == null) {
this.loggerService.warn(
'Trying to cancel a reservation without externalId',
{
reservation: reservation.id,
},
)
throw new CancellationError('No externalId')
}
if (reservation.status !== ReservationStatus.Booked) {
this.loggerService.warn('Trying to cancel a non-booked reservation', {
status: reservation.status,
})
throw new CancellationError('Not booked')
}
await this.init()
// @ts-expect-error TS doesn't see that externalId must be defined
await this.navigateToReservationCancellation(reservation)
await this.confirmCancellation()
}
public async addReservationToWaitingList( public async addReservationToWaitingList(
reservation: Reservation, reservation: Reservation,
timeSensitive = true, timeSensitive = true,
@ -1001,12 +916,6 @@ export class RunningReservationsNavigationError extends RunnerError {
} }
} }
export class RunningFutureReservationsNavigationError extends RunnerError {
constructor(error: Error) {
super(error, 'RunningFutureReservationsNavigationError')
}
}
export class RunnerWaitingListNavigationError extends RunnerError { export class RunnerWaitingListNavigationError extends RunnerError {
constructor(error: Error) { constructor(error: Error) {
super(error, 'RunnerWaitingListNavigationError') super(error, 'RunnerWaitingListNavigationError')
@ -1055,13 +964,6 @@ export class CourtNotAvailableError extends Error {
} }
} }
export class CancellationError extends Error {
constructor(message: string) {
super(message)
this.name = 'CancellationError'
}
}
export class RunnerOwnerSearchNetworkError extends RunnerError { export class RunnerOwnerSearchNetworkError extends RunnerError {
constructor(error: Error) { constructor(error: Error) {
super(error, 'RunnerOwnerSearchNetworkError') super(error, 'RunnerOwnerSearchNetworkError')
@ -1099,19 +1001,12 @@ export class RunnerReservationConfirmButtonError extends RunnerError {
super(error, 'RunnerReservationConfirmButtonError') super(error, 'RunnerReservationConfirmButtonError')
} }
} }
export class RunnerReservationConfirmSubmitError extends RunnerError { export class RunnerReservationConfirmSubmitError extends RunnerError {
constructor(error: Error) { constructor(error: Error) {
super(error, 'RunnerReservationConfirmSubmitError') super(error, 'RunnerReservationConfirmSubmitError')
} }
} }
export class RunnerReservationCancelButtonError extends RunnerError {
constructor(error: Error) {
super(error, 'RunnerReservationConfirmButtonError')
}
}
export class SessionStartError extends Error { export class SessionStartError extends Error {
constructor(message: string) { constructor(message: string) {
super(message) super(message)