fix: duplicate notification subscription & toast

This commit is contained in:
Pujit Mehrotra
2025-02-01 12:46:53 -05:00
parent b54660115e
commit c7a749efcd
10 changed files with 125 additions and 38 deletions

View File

@@ -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,

View File

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

View File

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

View File

@@ -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');

View File

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

View File

@@ -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,

View File

@@ -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<string, string>;
/** 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;
});
}
}

View File

@@ -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<void> {
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<string, string> = {};
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);

View File

@@ -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,
});
}
}
/**

View File

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