more docker compose adventures, better logging, readme clarrifications

This commit is contained in:
Egor 2026-01-18 14:46:17 -08:00
parent 8d3804593c
commit 1b4f1e9d0c
5 changed files with 78 additions and 51 deletions

View file

@ -55,6 +55,8 @@ docker compose up -d
### 3. ProtonMail Integration (Optional)
> **Note:** The default ProtonMail Bridge image uses `shenxn/protonmail-bridge:build` which compiles from source and supports multiple architectures. For x86_64 systems, you can use `shenxn/protonmail-bridge:latest` (pre-built binary, smaller and faster). For ARM devices (Raspberry Pi), stick with `:build`.
To receive ProtonMail notifications via Signal:
1. **Initialize ProtonMail Bridge** (one-time setup):

View file

@ -1,4 +1,3 @@
import chalk from 'chalk';
import { API_KEY, BRIDGE_IMAP_PASSWORD, BRIDGE_IMAP_USERNAME, PORT } from './constants/config';
import { ROUTES } from './constants/server';
import { checkSignalCli, initSignal, startDaemon } from './modules/signal';
@ -13,6 +12,7 @@ import {
handleUnregister,
} from './routes/unifiedpush';
import { withAuth, withFormAuth } from './utils/auth';
import { logError, logInfo, logSuccess, logWarn } from './utils/log';
let daemon: ReturnType<typeof Bun.spawn> | null = null;
@ -22,24 +22,28 @@ try {
const hasAccount = isLinked && (await initSignal({}));
if (hasAccount) {
console.log(chalk.green('✓ Signal account linked'));
logSuccess('✓ Signal account linked');
} else {
console.log(chalk.yellow('⚠ No Signal account linked'));
console.log(chalk.dim(` Visit http://localhost:${PORT}/link to link your device`));
logWarn('⚠ No Signal account linked');
logInfo(` Visit http://localhost:${PORT}/link to link your device`);
}
} catch (error) {
console.error(chalk.red('✗ Failed to start signal-cli daemon'));
console.error(chalk.dim(` ${error instanceof Error ? error.message : String(error)}`));
logError(` ${error instanceof Error ? error.message : String(error)}`);
}
if (!API_KEY) {
console.warn(chalk.yellow('⚠️ Server running without API_KEY'));
console.warn(chalk.dim(' Set API_KEY env var for production deployments.'));
logWarn('⚠️ Server running without API_KEY');
console.warn(' Set API_KEY env var for production deployments.');
}
if (BRIDGE_IMAP_USERNAME && BRIDGE_IMAP_PASSWORD) {
const { startProtonMonitor } = await import('./modules/protonmail');
await startProtonMonitor();
try {
const { startProtonMonitor } = await import('./modules/protonmail');
await startProtonMonitor();
} catch (err) {
logError('❌ Failed to start ProtonMail monitor:', err);
logWarn('⚠️ Continuing without ProtonMail integration');
}
}
const server = Bun.serve({
@ -99,4 +103,4 @@ const server = Bun.serve({
},
});
console.log(chalk.cyan.bold(`\n🚀 SUP running on http://localhost:${server.port}`));
logInfo(`\n🚀 SUP running on http://localhost:${server.port}`);

View file

@ -1,4 +1,3 @@
import chalk from 'chalk';
import Imap from 'imap';
import {
BRIDGE_IMAP_PASSWORD,
@ -9,32 +8,37 @@ import {
PROTON_BRIDGE_PORT,
SUP_TOPIC,
} from '../constants/config';
import { log } from '../utils/log';
import { logError, logInfo, logSuccess, logVerbose, logWarn } from '../utils/log';
import { createGroup, sendGroupMessage } from './signal';
import { getGroupId, register } from './store';
export async function startProtonMonitor() {
if (!BRIDGE_IMAP_USERNAME || !BRIDGE_IMAP_PASSWORD) {
console.error(
chalk.red('Missing required env vars: BRIDGE_IMAP_USERNAME and BRIDGE_IMAP_PASSWORD'),
);
console.error(chalk.yellow('Run: docker compose run --rm protonmail-bridge init'));
console.error(chalk.yellow('Then use `login` and `info` commands to get IMAP credentials'));
logError('Missing required env vars: BRIDGE_IMAP_USERNAME and BRIDGE_IMAP_PASSWORD');
logWarn('Run: docker compose run --rm protonmail-bridge init');
logWarn('Then use `login` and `info` commands to get IMAP credentials');
return;
}
log(chalk.blue(`🔗 Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`));
log(chalk.blue(`📨 Monitoring mailbox: ${BRIDGE_IMAP_USERNAME}`));
logInfo(`🔗 Connecting to Proton Bridge at ${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_PORT}`);
logInfo(`📨 Monitoring mailbox: ${BRIDGE_IMAP_USERNAME}`);
const imap = new Imap({
user: BRIDGE_IMAP_USERNAME,
password: BRIDGE_IMAP_PASSWORD,
host: PROTON_BRIDGE_HOST,
port: PROTON_BRIDGE_PORT,
tls: true,
tlsOptions: { rejectUnauthorized: false },
keepalive: true,
});
let imap: Imap;
try {
imap = new Imap({
user: BRIDGE_IMAP_USERNAME,
password: BRIDGE_IMAP_PASSWORD,
host: PROTON_BRIDGE_HOST,
port: PROTON_BRIDGE_PORT,
tls: true,
tlsOptions: { rejectUnauthorized: false },
keepalive: true,
});
} catch (err) {
logError('❌ Failed to initialize IMAP client:', err);
logWarn('⚠️ ProtonMail integration disabled (bridge not reachable)');
return;
}
async function sendNotification(title: string, message: string) {
try {
@ -50,23 +54,23 @@ export async function startProtonMonitor() {
: '';
await sendGroupMessage(groupId, `${prefix}**${title}**\n${message}`);
console.log(chalk.green(`✅ Notification sent: ${title}`));
logSuccess(`✅ Notification sent: ${title}`);
} catch (error) {
console.error(chalk.red('❌ Failed to send notification:'), error);
logError('❌ Failed to send notification:', error);
}
}
function openInbox() {
imap.openBox('INBOX', false, (err, box) => {
if (err) {
console.error(chalk.red('Failed to open inbox:'), err);
logError('Failed to open inbox:', err);
return;
}
log(`✅ Connected to inbox (${box.messages.total} messages)`);
logVerbose(`✅ Connected to inbox (${box.messages.total} messages)`);
imap.on('mail', async (numNewMsgs: number) => {
log(`📬 ${numNewMsgs} new message(s) received`);
logVerbose(`📬 ${numNewMsgs} new message(s) received`);
const fetch = imap.seq.fetch(`${box.messages.total}:*`, {
bodies: 'HEADER.FIELDS (FROM SUBJECT)',
@ -91,26 +95,38 @@ export async function startProtonMonitor() {
});
imap.on('update', () => {
log('📊 Mailbox updated');
logVerbose('📊 Mailbox updated');
});
});
}
imap.once('ready', () => {
log('✅ IMAP connection ready');
logVerbose('✅ IMAP connection ready');
openInbox();
});
imap.once('error', (err: Error) => {
console.error(chalk.red('❌ IMAP error:'), err);
logError('❌ IMAP error:', err);
logWarn('⚠️ ProtonMail integration disabled due to connection error');
});
imap.once('end', () => {
log('⚠️ IMAP connection ended, reconnecting...');
setTimeout(() => imap.connect(), 5000);
logVerbose('⚠️ IMAP connection ended, reconnecting...');
setTimeout(() => {
try {
imap.connect();
} catch (err) {
logError('❌ Failed to reconnect:', err);
}
}, 5000);
});
imap.connect();
try {
imap.connect();
} catch (err) {
logError('❌ Failed to connect to Proton Bridge:', err);
logWarn('⚠️ ProtonMail integration disabled');
}
process.on('SIGTERM', () => {
imap.end();

View file

@ -1,12 +1,11 @@
import { rm } from 'node:fs/promises';
import chalk from 'chalk';
import { DAEMON_START_MAX_ATTEMPTS, DEVICE_NAME, VERBOSE } from '../constants/config';
import { SIGNAL_CLI, SIGNAL_CLI_DATA, SIGNAL_CLI_SOCKET } from '../constants/paths';
import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from '../types';
import { log } from '../utils/log';
import { logError, logInfo, logSuccess, logVerbose, logWarn } from '../utils/log';
import { call } from '../utils/rpc';
log(`Running signal-cli from ${SIGNAL_CLI}`);
logVerbose(`Running signal-cli from ${SIGNAL_CLI}`);
let account: string | null = null;
let currentLinkUri: string | null = null;
@ -141,19 +140,19 @@ export async function startDaemon() {
if (VERBOSE) {
if (trimmed.includes('ERROR')) {
console.error(chalk.red('[signal-cli]'), trimmed);
logError('[signal-cli]', trimmed);
} else if (trimmed.includes('WARN')) {
console.warn(chalk.yellow('[signal-cli]'), trimmed);
logWarn('[signal-cli]', trimmed);
} else {
console.log(chalk.dim('[signal-cli]'), trimmed);
logInfo('[signal-cli]', trimmed);
}
continue;
}
if (trimmed.includes('WARN')) {
console.warn(chalk.yellow('[signal-cli]'), trimmed);
logWarn('[signal-cli]', trimmed);
} else if (trimmed.includes('ERROR') || !trimmed.includes('INFO')) {
console.error(chalk.red('[signal-cli]'), trimmed);
logError('[signal-cli]', trimmed);
}
}
})();
@ -169,11 +168,11 @@ export async function startDaemon() {
},
});
socket.end();
console.log(chalk.green('✓ signal-cli daemon started'));
logSuccess('✓ signal-cli daemon started');
return proc;
} catch (_error) {
if (authError && attempts > 5 && !cleaned) {
console.log(chalk.yellow('⚠ Detected stale account data, cleaning up and retrying...'));
logWarn('⚠ Detected stale account data, cleaning up and retrying...');
proc.kill();
await unlinkDevice();
cleaned = true;

View file

@ -1,3 +1,9 @@
import chalk from 'chalk';
import { VERBOSE } from '../constants/config';
export const log = (...args: unknown[]) => VERBOSE && console.log(...args);
export const logVerbose = (...args: unknown[]) => VERBOSE && console.log(...args);
export const logError = (...args: unknown[]) => console.error(chalk.red(...args));
export const logWarn = (...args: unknown[]) => console.warn(chalk.yellow(...args));
export const logInfo = (...args: unknown[]) => console.log(chalk.blue(...args));
export const logSuccess = (...args: unknown[]) => console.log(chalk.green(...args));