Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/helpers/iparking_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ interface CarParksFreeSlotResponse {
}

async function fetchWithToken<T>(endpoint: string): Promise<T> {
const token = env.get("API_ACCESS_TOKEN");
const token = env.get("PARKING_API_ACCESS_TOKEN");

const controller = new AbortController();
const timeoutId = setTimeout(() => {
Expand Down
17 changes: 12 additions & 5 deletions commands/synchronize_parking_slots.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { toIBaseError } from "@solvro/error-handling/base";
import {
analyzeErrorStack,
prepareReportForLogging,
} from "@solvro/error-handling/reporting";
import { DateTime } from "luxon";

import { BaseCommand } from "@adonisjs/core/ace";
Expand Down Expand Up @@ -45,10 +50,12 @@ export default class SynchronizeParkingSlots extends BaseCommand {
try {
freeSlots = await getCarParksFreeSlots();
} catch (error) {
const report = analyzeErrorStack(toIBaseError(error));
this.logger.error(
`Failed to fetch parking slots from iParking: ${error}`,
`Failed to fetch parking slots from iParking: ${prepareReportForLogging(report)}`,
);
this.exitCode = 1;
this.error = error as unknown;
return;
}
freeSlots = freeSlots.filter(
Expand Down Expand Up @@ -79,12 +86,12 @@ export default class SynchronizeParkingSlots extends BaseCommand {
);

if (unknownExternalIds.length > 0) {
this.logger.error(
const error =
`Missing parkings for external IDs: ${unknownExternalIds.join(", ")}. ` +
'Run "node ace synchronize:parkings" first.',
);

'Run "node ace synchronize:parkings" first.';
this.logger.error(error);
this.exitCode = 2;
this.error = error;
return;
}

Expand Down
8 changes: 7 additions & 1 deletion commands/synchronize_parkings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { toIBaseError } from "@solvro/error-handling/base";
import {
analyzeErrorStack,
prepareReportForLogging,
} from "@solvro/error-handling/reporting";
import { nullableMap } from "@solvro/utils/option";
import { DateTime } from "luxon";

Expand Down Expand Up @@ -76,8 +81,9 @@ export default class SynchronizeParkings extends BaseCommand {
this.mapToParkingModel(carPark),
);
} catch (error) {
const report = analyzeErrorStack(toIBaseError(error));
this.logger.warning(
`Failed to synchronize parking "${carPark.symbol}" (external ID: ${carPark.id}): ${error instanceof Error ? error.message : String(error)}`,
`Failed to synchronize parking "${carPark.symbol}" (external ID: ${carPark.id}): ${prepareReportForLogging(report)}`,
);
this.exitCode = 1;
}
Expand Down
80 changes: 77 additions & 3 deletions commands/synchronize_parkings_with_slots.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { BaseCommand } from "@adonisjs/core/ace";
import { toIBaseError } from "@solvro/error-handling/base";
import {
analyzeErrorStack,
prepareReportForLogging,
} from "@solvro/error-handling/reporting";
import { DateTime } from "luxon";

import { BaseCommand, flags } from "@adonisjs/core/ace";
import type { CommandOptions } from "@adonisjs/core/types/ace";

import env from "#start/env";

interface DiscordWebhookPayload {
title: string;
description: string;
/** Event time as an ISO 8601 string, e.g. "2026-11-06T12:00:00Z". */
timestamp: string;
}

export default class SynchronizeParkingsWithSlots extends BaseCommand {
static commandName = "synchronize:parkings-with-slots";
static description =
Expand All @@ -10,6 +26,12 @@ export default class SynchronizeParkingsWithSlots extends BaseCommand {
startApp: true,
};

@flags.boolean({
alias: "w",
description: "Enable discord webhooks on command error",
})
declare webhook: boolean;

async run() {
this.logger.info(`Start "${this.commandName}" command`);
let slotsCommand = await this.kernel.exec("synchronize:parkings-slots", []);
Expand All @@ -22,15 +44,67 @@ export default class SynchronizeParkingsWithSlots extends BaseCommand {
);

if (parkingsCommand.exitCode !== 0) {
this.logger.error(`"${this.commandName}" failed`);
this.exitCode = parkingsCommand.exitCode;
this.error = parkingsCommand.error as unknown;
return;
}

// Retry once after synchronizing car parks and propagate the retry result.
slotsCommand = await this.kernel.exec("synchronize:parkings-slots", []);
}

this.exitCode = slotsCommand.exitCode;
this.logger.info(`"${this.commandName}" completed`);
this.error = slotsCommand.error as unknown;
}

async completed() {
if (this.exitCode === 0) {
this.logger.info(`"${this.commandName}" completed`);
return;
}

this.logger.error(`"${this.commandName}" failed`);

if (this.webhook) {
const title = `backend-topwr-parking: command ${this.commandName} has failed`;

const description =
this.error === null || this.error === undefined
? `Command exited with code ${this.exitCode}`
: prepareReportForLogging(
analyzeErrorStack(toIBaseError(this.error)),
);

// : `Command exited with code ${this.exitCode}`;a

Comment thread
krzys13 marked this conversation as resolved.
Resolved
try {
const payload: DiscordWebhookPayload = {
title: `❌ [ERROR]: ${title}`,
description,
timestamp: DateTime.now().toISO(),
};
const response = await fetch(
env.get("DISORD_WEBHOOK_URL") as unknown as string,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ embeds: [payload] }),
},
);

if (!response.ok) {
this.logger.warning(
`Failed to send Discord notification: HTTP ${response.status}`,
);
}
} catch (notificationError) {
const report = analyzeErrorStack(toIBaseError(notificationError));
this.logger.warning(
`Failed to send Discord notification: ${prepareReportForLogging(report)}`,
);
}
}
}
}
Loading
Loading