From c7a749efcdee4c562966b6fc86ae385d1b329776 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Sat, 1 Feb 2025 12:46:53 -0500 Subject: [PATCH] fix: duplicate notification subscription & toast --- api/ecosystem.config.json | 1 + api/justfile | 44 +++++++++++++++++ api/scripts/deploy-dev.sh | 15 ------ api/src/index.ts | 1 + api/src/unraid-api/auth/auth.guard.ts | 5 +- api/src/unraid-api/cli/cli.module.ts | 2 + api/src/unraid-api/cli/pm2.service.ts | 47 +++++++++++++++++++ api/src/unraid-api/cli/start.command.ts | 37 ++++++++------- .../notifications/notifications.service.ts | 10 ++-- web/components/Notifications/Sidebar.vue | 1 + 10 files changed, 125 insertions(+), 38 deletions(-) create mode 100644 api/src/unraid-api/cli/pm2.service.ts diff --git a/api/ecosystem.config.json b/api/ecosystem.config.json index 8b1b2e2e7..2636fc5ba 100644 --- a/api/ecosystem.config.json +++ b/api/ecosystem.config.json @@ -6,6 +6,7 @@ "script": "./dist/main.js", "cwd": "/usr/local/unraid-api", "interpreter": "bun", + "interpreter_args": "--smol", "exec_mode": "fork", "wait_ready": true, "listen_timeout": 15000, diff --git a/api/justfile b/api/justfile index 2ce5bf561..fb1efa94f 100644 --- a/api/justfile +++ b/api/justfile @@ -16,6 +16,50 @@ setup: @deploy: ./scripts/deploy-dev.sh +alias r:= restart + +# restarts the api on {target_server} +restart target_server="" : + #!/usr/bin/env bash + # Path to store the last used server name + state_file="$HOME/.deploy_state" + + # Read the last used server name from the state file + if [[ -f "$state_file" ]]; then + last_server_name=$(cat "$state_file") + else + last_server_name="" + fi + + # Read the server name from the command-line argument or use the last used server name as the default + server_name=$([ "{{ target_server }}" = "" ] && echo "$last_server_name" || echo ""{{ target_server }}"") + + # Check if the server name is provided + if [[ -z "$server_name" ]]; then + echo "Please provide the SSH server name." + exit 1 + fi + + # Save the current server name to the state file + echo "$server_name" > "$state_file" + # Run unraid-api restart on remote host + ssh root@"${server_name}" "LOG_LEVEL=trace unraid-api restart" + # build & deploy bd: build deploy +# plays an os-specific bell +[no-cd] +chime: + #!/usr/bin/env bash + # Play built-in sound based on the operating system + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + afplay /System/Library/Sounds/Glass.aiff + elif [[ "$OSTYPE" == "linux-gnu" ]]; then + # Linux + paplay /usr/share/sounds/freedesktop/stereo/complete.oga + elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + # Windows + powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()" + fi diff --git a/api/scripts/deploy-dev.sh b/api/scripts/deploy-dev.sh index 04f5e6b5e..3cf7c00cd 100755 --- a/api/scripts/deploy-dev.sh +++ b/api/scripts/deploy-dev.sh @@ -48,21 +48,6 @@ exit_code=$? # Chown the directory ssh root@"${server_name}" "chown -R root:root /usr/local/unraid-api" -# Run unraid-api restart on remote host -ssh root@"${server_name}" "INTROSPECTION=true LOG_LEVEL=trace unraid-api restart" - -# Play built-in sound based on the operating system -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS - afplay /System/Library/Sounds/Glass.aiff -elif [[ "$OSTYPE" == "linux-gnu" ]]; then - # Linux - paplay /usr/share/sounds/freedesktop/stereo/complete.oga -elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then - # Windows - powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()" -fi - # Exit with the rsync command's exit code exit $exit_code diff --git a/api/src/index.ts b/api/src/index.ts index 61f5677c0..45bcf2fae 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -105,6 +105,7 @@ export const viteNodeApp = async () => { }, { wait: 9999 } ); + await new Promise(() => {}); } catch (error: unknown) { if (error instanceof Error) { logger.error(error, 'API-ERROR'); diff --git a/api/src/unraid-api/auth/auth.guard.ts b/api/src/unraid-api/auth/auth.guard.ts index 2a4f026ec..fb6024f0e 100644 --- a/api/src/unraid-api/auth/auth.guard.ts +++ b/api/src/unraid-api/auth/auth.guard.ts @@ -86,8 +86,11 @@ export class GraphqlAuthGuard // parse cookies from raw headers on initial web socket connection request if (fullContext.connectionParams) { + console.log(JSON.stringify(fullContext.req, null, 2)); const rawHeaders = fullContext.req.extra.request.rawHeaders; - const headerIndex = rawHeaders.findIndex((headerOrValue) => headerOrValue === 'Cookie'); + const headerIndex = rawHeaders.findIndex( + (headerOrValue) => headerOrValue.toLowerCase() === 'cookie' + ); const cookieString = rawHeaders[headerIndex + 1]; request.cookies = parseCookies(cookieString); } diff --git a/api/src/unraid-api/cli/cli.module.ts b/api/src/unraid-api/cli/cli.module.ts index 4625bf451..8a8eeb087 100644 --- a/api/src/unraid-api/cli/cli.module.ts +++ b/api/src/unraid-api/cli/cli.module.ts @@ -8,6 +8,7 @@ import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.comman import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions'; import { LogService } from '@app/unraid-api/cli/log.service'; import { LogsCommand } from '@app/unraid-api/cli/logs.command'; +import { PM2Service } from '@app/unraid-api/cli/pm2.service'; import { ReportCommand } from '@app/unraid-api/cli/report.command'; import { RestartCommand } from '@app/unraid-api/cli/restart.command'; import { AddSSOUserCommand } from '@app/unraid-api/cli/sso/add-sso-user.command'; @@ -31,6 +32,7 @@ import { VersionCommand } from '@app/unraid-api/cli/version.command'; RemoveSSOUserQuestionSet, ListSSOUserCommand, LogService, + PM2Service, StartCommand, StopCommand, RestartCommand, diff --git a/api/src/unraid-api/cli/pm2.service.ts b/api/src/unraid-api/cli/pm2.service.ts new file mode 100644 index 000000000..5bba1f030 --- /dev/null +++ b/api/src/unraid-api/cli/pm2.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; + +import type { Options, Result } from 'execa'; +import { execa } from 'execa'; + +import { LogService } from '@app/unraid-api/cli/log.service'; + +type CmdContext = { + tag: string; + env?: Record; + /** Default: false. + * + * When true, results will not be automatically handled and logged. + * The caller must handle desired effects. + */ + raw?: boolean; +}; + +@Injectable() +export class PM2Service { + constructor(private readonly logger: LogService) {} + + /** + * Executes a PM2 command with the provided arguments and environment variables. + * + * @param context - An object containing a tag for logging purposes and optional environment variables (merging with current env). + * @param args - Arguments to pass to the PM2 command. Each arguement is escaped. + * @returns A promise that resolves to a Result object containing the command's output. + * Logs debug information on success and error details on failure. + */ + async run(context: CmdContext, ...args: string[]) { + const { tag, env, raw = false } = context; + const runCommand = () => execa('bun', ['x', 'pm2', ...args], { env } satisfies Options); + if (raw) { + return runCommand(); + } + return runCommand() + .then((result) => { + this.logger.debug(result.stdout); + return result; + }) + .catch((result: Result) => { + this.logger.error(`PM2 error occurred from tag "${tag}": ${result.stdout}\n`); + return result; + }); + } +} diff --git a/api/src/unraid-api/cli/start.command.ts b/api/src/unraid-api/cli/start.command.ts index e8c9c3954..c96ea7a24 100644 --- a/api/src/unraid-api/cli/start.command.ts +++ b/api/src/unraid-api/cli/start.command.ts @@ -5,6 +5,7 @@ import type { LogLevel } from '@app/core/log'; import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts'; import { levels } from '@app/core/log'; import { LogService } from '@app/unraid-api/cli/log.service'; +import { PM2Service } from '@app/unraid-api/cli/pm2.service'; interface StartCommandOptions { 'log-level'?: string; @@ -12,38 +13,38 @@ interface StartCommandOptions { @Command({ name: 'start' }) export class StartCommand extends CommandRunner { - constructor(private readonly logger: LogService) { + constructor( + private readonly logger: LogService, + private readonly pm2: PM2Service + ) { super(); } async cleanupPM2State() { - await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH]) - .then(({ stdout }) => this.logger.debug(stdout)) - .catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr)); - await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH]) - .then(({ stdout }) => this.logger.debug(stdout)) - .catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr)); - - // Update PM2 - await execa(PM2_PATH, ['update']) - .then(({ stdout }) => this.logger.debug(stdout)) - .catch(({ stderr }) => this.logger.error('PM2 Update Error: ' + stderr)); + await this.pm2.run({ tag: 'PM2 Stop' }, 'stop', ECOSYSTEM_PATH); + await this.pm2.run({ tag: 'PM2 Delete' }, 'delete', ECOSYSTEM_PATH); + await this.pm2.run({ tag: 'PM2 Update' }, 'update'); } async run(_: string[], options: StartCommandOptions): Promise { this.logger.info('Starting the Unraid API'); await this.cleanupPM2State(); - const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : ''; - const { stderr, stdout } = await execa(`${envLog} ${PM2_PATH}`.trim(), [ + + const env: Record = {}; + if (options['log-level']) { + env.LOG_LEVEL = options['log-level']; + } + const { stderr, stdout } = await this.pm2.run( + { tag: 'PM2 Start', env, raw: true }, 'start', ECOSYSTEM_PATH, - '--update-env', - ]); + '--update-env' + ); if (stdout) { - this.logger.log(stdout); + this.logger.log(stdout.toString()); } if (stderr) { - this.logger.error(stderr); + this.logger.error(stderr.toString()); process.exit(1); } process.exit(0); diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts index f61169749..b9e96bb1b 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts @@ -103,10 +103,12 @@ export class NotificationsService { const notification = await this.loadNotificationFile(path, NotificationType[type]); this.increment(notification.importance, NotificationsService.overview[type.toLowerCase()]); - this.publishOverview(); - pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_ADDED, { - notificationAdded: notification, - }); + if (type === NotificationType.UNREAD) { + this.publishOverview(); + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_ADDED, { + notificationAdded: notification, + }); + } } /** diff --git a/web/components/Notifications/Sidebar.vue b/web/components/Notifications/Sidebar.vue index f89bbe113..c6f3f1cf7 100644 --- a/web/components/Notifications/Sidebar.vue +++ b/web/components/Notifications/Sidebar.vue @@ -42,6 +42,7 @@ const { onResult: onNotificationAdded } = useSubscription(notificationAddedSubsc onNotificationAdded(({ data }) => { if (!data) return; const notif = useFragment(NOTIFICATION_FRAGMENT, data.notificationAdded); + if (notif.type !== NotificationType.Unread) return; // probably smart to leave this log outside the if-block for the initial release console.log('incoming notification', notif);