Compare commits

...

6 Commits

Author SHA1 Message Date
Cursor Agent
6debbf77b6 [AI] Remove temporary screenshots
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-26 20:16:19 +00:00
Cursor Agent
235535a68f [AI] Add notification screenshots for PR review
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-26 20:15:55 +00:00
Cursor Agent
358549bdd4 [AI] Show sync performance notification when message count exceeds 20k
- Add messageCount to sync success event type
- Count messages_crdt rows after successful sync on the server
- Show dismissible info notification with 'Reset sync' button when >20k messages
- Persist dismissal state in localStorage (budget-scoped) so it doesn't reappear
- Add flags.syncPerformanceNotificationDismissed to LocalPrefs type

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-25 23:10:20 +00:00
Michael Clark
f97a89dc28 🐛 Fix file path on windows (#7076)
* fix file path on windows

* file path in migrations

* release notes
2026-02-25 15:00:10 +00:00
Juulz
a4bd301ec6 🐞 Midnight theme: Change menuAutoCompleteTextHover color - Fixes #7029 (#7048)
* Change menuAutoCompleteTextHover color to green400

* Change menuAutoCompleteTextHover color to green400 in Midnight theme.

Change menuAutoCompleteTextHover color to green400 in Midnight theme.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-24 17:29:09 +00:00
Julian Dominguez-Schatz
18072e1d8b Validate file IDs for correctness (#7067)
* Validate file IDs for correctness

* Add release notes
2026-02-24 15:32:50 +00:00
11 changed files with 105 additions and 3 deletions

View File

@@ -74,7 +74,7 @@ export const menuKeybindingText = colorPalette.purple200;
export const menuAutoCompleteBackground = colorPalette.gray600;
export const menuAutoCompleteBackgroundHover = colorPalette.gray500;
export const menuAutoCompleteText = colorPalette.gray100;
export const menuAutoCompleteTextHover = colorPalette.green900;
export const menuAutoCompleteTextHover = colorPalette.green400;
export const menuAutoCompleteTextHeader = colorPalette.purple200;
export const menuAutoCompleteItemTextHover = colorPalette.gray50;
export const menuAutoCompleteItemText = menuItemText;

View File

@@ -59,6 +59,42 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
);
}
if (
event.type === 'success' &&
event.messageCount != null &&
event.messageCount > 20000
) {
const budgetId = prefs?.id;
const dismissedKey = `${budgetId}-flags.syncPerformanceNotificationDismissed`;
const dismissed = localStorage.getItem(dismissedKey);
if (!dismissed || dismissed !== 'true') {
store.dispatch(
addNotification({
notification: {
type: 'message',
title: t('Sync performance issue'),
message: t(
'Your budget has {{messageCount}} sync messages which may cause slow performance. Resetting sync will clear old messages and speed things up.',
{ messageCount: event.messageCount.toLocaleString() },
),
sticky: true,
id: 'sync-performance',
button: {
title: t('Reset sync'),
action: () => {
void store.dispatch(resetSync());
},
},
onClose: () => {
localStorage.setItem(dismissedKey, JSON.stringify(true));
},
},
}),
);
}
}
const tables = event.tables;
if (tables.includes('prefs')) {

View File

@@ -638,10 +638,18 @@ export const fullSync = once(async function (): Promise<
const tables = getTablesFromMessages(messages);
const messageCountResult = db.runQuery<{ count: number }>(
'SELECT COUNT(*) as count FROM messages_crdt',
[],
true,
);
const messageCount = messageCountResult[0]?.count ?? 0;
app.events.emit('sync', {
type: 'success',
tables,
syncDisabled: checkSyncingMode('disabled'),
messageCount,
});
return { messages };
});

View File

@@ -80,6 +80,7 @@ export type LocalPrefs = Partial<{
'budget.showHiddenCategories': boolean;
'budget.startMonth': string;
'flags.updateNotificationShownForVersion': string;
'flags.syncPerformanceNotificationDismissed': boolean;
'schedules.showCompleted': boolean;
reportsViewLegend: boolean;
reportsViewSummary: boolean;

View File

@@ -30,6 +30,7 @@ type SyncEvent = {
type: 'success';
tables: string[];
syncDisabled?: boolean;
messageCount?: number;
}
| {
type: 'error';

View File

@@ -366,6 +366,19 @@ describe('/upload-user-file', () => {
expect(res.text).toBe('fileId is required');
});
it('returns 400 for invalid fileId format', async () => {
const res = await request(app)
.post('/upload-user-file')
.set('Content-Type', 'application/encrypted-file')
.set('x-actual-token', 'valid-token')
.set('x-actual-name', 'test-file')
.set('x-actual-file-id', 'budget@2026')
.send(Buffer.from('file content'));
expect(res.statusCode).toEqual(400);
expect(res.text).toBe('invalid fileId');
});
it('uploads a new file successfully', async () => {
const fileId = crypto.randomBytes(16).toString('hex');
const fileName = 'test-file.txt';
@@ -670,6 +683,16 @@ describe('/download-user-file', () => {
expect(res.text).toBe('User or file not found');
});
it('returns 400 for invalid fileId format', async () => {
const res = await request(app)
.get('/download-user-file')
.set('x-actual-token', 'valid-token')
.set('x-actual-file-id', 'budget@2026');
expect(res.statusCode).toEqual(400);
expect(res.text).toBe('invalid fileId');
});
it('returns 500 error if the file does not exist on the filesystem', async () => {
getAccountDb().mutate(
'INSERT INTO files (id, deleted) VALUES (?, FALSE)',

View File

@@ -49,11 +49,16 @@ app.use(express.json({ limit: `${config.get('upload.fileSizeLimitMB')}mb` }));
export { app as handlers };
const OK_RESPONSE = { status: 'ok' };
const FILE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
function boolToInt(deleted) {
return deleted ? 1 : 0;
}
function isValidFileId(fileId: unknown): fileId is string {
return typeof fileId === 'string' && FILE_ID_PATTERN.test(fileId);
}
const verifyFileExists = (fileId, filesService, res, errorObject) => {
try {
return filesService.get(fileId);
@@ -256,6 +261,10 @@ app.post('/upload-user-file', async (req, res) => {
res.status(400).send('fileId is required');
return;
}
if (!isValidFileId(fileId)) {
res.status(400).send('invalid fileId');
return;
}
let groupId = req.headers['x-actual-group-id'] || null;
const encryptMeta = req.headers['x-actual-encrypt-meta'] || null;
@@ -352,6 +361,10 @@ app.get('/download-user-file', async (req, res) => {
res.status(400).send('Single file ID is required');
return;
}
if (!isValidFileId(fileId)) {
res.status(400).send('invalid fileId');
return;
}
const filesService = new FilesService(getAccountDb());
const file = verifyFileExists(

View File

@@ -1,6 +1,6 @@
import { readdir } from 'node:fs/promises';
import path, { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { load } from 'migrate';
@@ -30,7 +30,9 @@ export async function run(direction: 'up' | 'down' = 'up'): Promise<void> {
for (const f of files
.filter(f => f.endsWith('.js') || f.endsWith('.ts'))
.sort()) {
migrationsModules[f] = await import(path.join(migrationsDir, f));
migrationsModules[f] = await import(
pathToFileURL(path.join(migrationsDir, f)).href
);
}
return new Promise<void>((resolve, reject) => {

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [Juulz]
---
Change menuAutoCompleteTextHover color to green400 in Midnight theme.

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [jfdoming]
---
Validate file IDs for correctness

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [MikesGlitch]
---
Fix server migrations when running on Windows