Skip to content

Feat/backup#14

Merged
carnach merged 12 commits into
releasefrom
feat/backup
Feb 1, 2026
Merged

Feat/backup#14
carnach merged 12 commits into
releasefrom
feat/backup

Conversation

@carnach
Copy link
Copy Markdown
Owner

@carnach carnach commented Feb 1, 2026

No description provided.

…ckup/restore system

Development Enhancements:
- Added `dev:vercel` script to package.json for local Vercel development environment
- Integrated Vercel Analytics for web traffic and performance monitoring
- Added Analytics component to root layout for automatic tracking

Backup/Restore System:
Implemented a complete group backup and restore system with intelligent version management:

Core Features:
- Full group data export as ZIP archive containing:
  * Group metadata (name, currency, information)
  * All participants with their details
  * Complete expense records with notes, documents, and recurring expense links
  * Activity log entries
  * Export metadata with version and timestamp

Restore Operations:
- Three operation modes based on version comparison:
  1. CREATE: Import backup as new group when group doesn't exist
  2. UPDATE: Merge newer data from backup into existing group
  3. ROLLBACK: Replace all current data with older backup version

Technical Implementation:
- Version comparison using latest expense/activity timestamps
- Transactional restore operations using Prisma for data integrity
- Detailed difference calculation (added/removed expenses and participants)
- ZIP format with backup.json and metadata.json files
- Type-safe implementation with comprehensive error handling

User Interface:
- Export dropdown menu with "Create Backup" option
- RestoreBackupButton component on groups page with DropdownMenu containing MoreVertical icon
- Make RestoreBackupButton dialog controllable via open/onOpenChange props
- Update translations to include restoreBackup key in Groups section- Interactive workflow with analysis, confirmation, and progress states
- Clear visual feedback for version comparison results
- Warning dialogs for destructive operations (rollback)

Files Added:
- src/app/groups/[groupId]/backup/export/route.ts (121 lines)
- src/app/groups/backup/import/route.ts (123 lines)
- src/lib/backup.ts (371 lines)
- src/components/restore-backup-button.tsx (313 lines)

Files Modified:
- package.json: Added dev:vercel script, @vercel/analytics, jszip dependencies
- src/app/layout.tsx: Integrated Analytics component
- src/app/groups/[groupId]/export-button.tsx: Added backup option to export dropdown
- src/app/groups/recent-group-list.tsx: Added restore backup button
- messages/en-US.json: Complete backup/restore translations (18 keys)

Dependencies Added:
- @vercel/analytics@1.6.1: Web analytics integration
- jszip@3.10.1: ZIP archive creation for backup packaging

- Replace standalone RestoreBackupButton with DropdownMenu containing MoreVertical icon
- Make RestoreBackupButton dialog controllable via open/onOpenChange props
- Add "Restore Backup" menu item to Groups page header dropdown
- Update translations to include restoreBackup key in Groups section

This provides a cleaner groups page header while maintaining easy access to backup restoration functionality.
- Add JSON import endpoint (src/app/groups/json/import/route.ts)
  - Analyze existing vs new group data
  - Restore/create groups in 60s transaction timeout
  - Support create, update, and rollback modes with version comparison

- Add core import logic (src/lib/json-import.ts)
  - Version comparison using expense dates
  - Differential updates for incremental imports
  - Participant ID validation before expense creation
  - Activity tracking with import date metadata

- Add undo last import feature (src/app/groups/[groupId]/data/delete/route.ts)
  - Remove all expenses/participants/activities created in last import
  - Only available when JSON_IMPORT_START marker exists

- Add import marker detection (src/app/api/groups/[groupId]/has-import-marker/route.ts)
  - Checks for JSON_IMPORT_START activity to show undo button
  - Conditional rendering of delete options

- Add UI components
  - ImportJSONButton: Dialog with analyze/restore/rollback workflow
  - DeleteDataButton: Undo import only (conditional on import marker)
  - Activity display: Shows 'Import (date)' for system-created entries

- Activity improvements
  - JSON-embed import date and expense title in activity.data
  - Set activity.time to importTime for consistency in undo logic
  - Parse and display import date in activity list UI

- UX/localization
  - Add JSONImport translations (23 keys)
  - Update DeleteData translations (undo-only mode)
  - Remove clear-all option from UI
  - Add ImportJSONButton to recent groups menu

- Bug fixes
  - Fixed null value warning in expense form (value={field.value ?? ''})
  - Removed debug endpoints
  - Removed console logging from import logic

Technical notes:
- Transaction timeout: 60s (from 5s default) for large imports
- Import activities timestamped at importTime for accurate undo
- Participant ID validation prevents orphaned expense data
- Version comparison uses latest expense date as proxy
- Differential updates merge with existing participants/expenses
- Revert activity entries to use their original expense dates for proper chronological display
- Keep import date in activity metadata shown in brackets after 'Import' label
- Applies to all import modes (create, update, rollback)
- Individual expenses now appear at their original dates while maintaining import timestamp reference
- Reorder menu items: Remove from Recent Groups, Archive Group, then Create Backup
- Places destructive action first, then export at the bottom
Features Added:
- Delete group functionality with permanent deletion warning dialog
- Optional S3 document deletion when deleting groups
- Document URL validation during backup restore
- Backup export moved from expenses dropdown to recent groups menu
- JSON import activity message clarified

Delete Group:
- Add 'Delete group permanently' option to recent groups three-dots menu
- Create confirmation dialog with backup recommendation
- Query and display S3 document count if present
- Warn that cloud images don't form part of backup
- Optional checkbox to delete S3 images when deleting group
- Implement deleteGroupWithDocuments API with S3Client integration
- Create TRPC procedures for delete and document count

Backup Restore Improvements:
- Check document URLs exist before restoring
- Skip missing documents and continue operation
- Collect and return warnings for unavailable resources
- Display warnings to user via alert dialog
- Update restoreGroupFromBackup signature to return warnings

UI/UX Improvements:
- Move 'Create Backup' from expenses export dropdown to groups menu
- Position at bottom of menu after Archive Group option
- Fix malformed JSON in en-US translations (RecentRemovedToast)
- Update JSON import message: 'Activity history is not preserved, it will be regenerated'
- Fix expense form indentation (formatting cleanup)

Technical Details:
- Add AWS SDK S3Client for document deletion
- Implement checkUrlExists helper using fetch HEAD requests
- Add getGroupDocumentCount API and TRPC procedure
- Update deleteGroup to deleteGroupWithDocuments with optional flag
- Handle missing S3 credentials gracefully
Architectural Improvements:
- Move S3 deletion logic to server actions (delete-group-actions.ts)
- Use 'use server' directive to safely handle env variables
- Prevent ZodError in client by keeping env imports server-side only
- Separate concerns: database operations in api.ts, S3 in server actions

S3 Document Management:
- Fix document ID preservation during expense creation (use doc.id instead of randomId())
- Add S3 cleanup when deleting individual expenses
- Add S3 cleanup when deleting groups with deleteDocuments flag
- Ensure ExpenseDocument table entries are deleted alongside S3 objects
- Implement helper function deleteS3DocumentsByUrls for reusability

Database Cleanup:
- Explicitly delete ExpenseDocument records before deleting expenses
- Explicitly delete ExpenseDocument records before deleting groups
- Prevent orphaned document references in database
- Use upsert for backup restore to handle duplicate documents gracefully

Backup/Restore:
- Fix duplicate document creation error during backup restore
- Use upsert instead of create to handle existing documents
- Maintain document-expense relationships properly during restore

Recent Groups UI:
- Reorder dropdown menu for better UX:
  1. Create Backup
  2. Archive Group
  3. Remove from recent groups
  4. Delete group permanently
- Add icons to all menu items (Archive, ArchiveX, X, Trash2)
- Remove deleted group from recent list automatically
- Refresh groups list after permanent deletion

TRPC Procedures:
- Update delete group procedure to call S3 server action
- Update delete expense procedure to call S3 server action
- Maintain proper order: S3 deletion → document cleanup → entity deletion

Technical Details:
- Server action: deleteGroupS3Documents(groupId)
- Server action: deleteExpenseS3Documents(expenseId)
- Helper: deleteS3DocumentsByUrls(urls[])
- Prevents client-side env variable access
- Maintains separation of concerns
- All deletions now properly cascade through S3 and database
Copilot AI review requested due to automatic review settings February 1, 2026 08:47
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
my-spliit-instance Ready Ready Preview, Comment Feb 1, 2026 9:35am

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive backup and restore functionality for groups, along with S3 document cleanup capabilities and Vercel Analytics integration. The feature allows users to export full backups (as ZIP files with complete data), import from JSON exports (with limited data), restore backups, and delete groups with optional document cleanup.

Changes:

  • Added backup export/import functionality with ZIP file support (full backup) and JSON import (limited format)
  • Implemented group deletion with optional S3 document cleanup and document count display
  • Added Vercel Analytics integration for usage tracking

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 24 comments.

Show a summary per file
File Description
src/lib/backup.ts Core backup logic for creating and restoring full group backups with documents and activities
src/lib/json-import.ts JSON import logic for migrating groups from other Spliit instances (limited data support)
src/lib/api.ts Added group deletion functions and document count queries; modified expense creation to use doc.id
src/app/groups/backup/import/route.ts API route for analyzing and restoring ZIP backup files
src/app/groups/[groupId]/backup/export/route.ts API route for exporting complete group backups as ZIP files
src/app/groups/json/import/route.ts API route for analyzing and importing JSON exports
src/app/groups/[groupId]/data/delete/route.ts API route for undoing last JSON import
src/app/api/groups/[groupId]/has-import-marker/route.ts API route to check if group has imported data
src/app/groups/delete-group-actions.ts Server actions for S3 document deletion (safely accesses env variables)
src/trpc/routers/groups/delete.procedure.ts tRPC procedure for deleting groups with optional document cleanup
src/trpc/routers/groups/get-document-count.procedure.ts tRPC procedure to get document count for a group
src/trpc/routers/groups/expenses/delete.procedure.ts Modified to delete S3 documents when deleting expenses
src/components/restore-backup-button.tsx UI component for restoring backups with version comparison
src/components/import-json-button.tsx UI component for importing JSON exports with warnings
src/components/delete-group-dialog.tsx Dialog for deleting groups with document deletion option
src/components/delete-data-button.tsx Button to undo last import
src/app/groups/[groupId]/activity/activity-item.tsx Modified to display import activities with JSON-parsed data
src/app/layout.tsx Added Vercel Analytics component
package.json Added @vercel/analytics and jszip dependencies
messages/en-US.json Added translations for backup/import UI

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +73
// Delete participants - Note: Can't filter by createdAt as it doesn't exist
// We'll keep participants to avoid breaking expense references
// Alternatively, delete only participants not referenced by remaining expenses
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at lines 71-73 indicates awareness that participants can't be deleted by creation time, but leaves them in the database. This creates data inconsistency where imported participants remain after "undoing" an import. These orphaned participants could:

  1. Confuse users who expect a complete undo
  2. Cause issues if the user tries to re-import with different participant IDs
  3. Accumulate over multiple import/undo cycles

Consider implementing participant cleanup by: 1) Deleting participants that have no associated expenses after the expense cleanup, 2) Adding a createdAt field to the Participant model, or 3) Tracking imported participants in a separate table or using activity markers.

Suggested change
// Delete participants - Note: Can't filter by createdAt as it doesn't exist
// We'll keep participants to avoid breaking expense references
// Alternatively, delete only participants not referenced by remaining expenses
// Delete participants that are no longer referenced by any remaining expenses
// This avoids leaving orphaned participants after undoing an import.
await tx.participant.deleteMany({
where: {
groupId: groupId,
// Only delete participants that have no associated expenses
// (and no associated expense-paid-for entries, if such a relation exists).
expenses: {
none: {},
},
expensePaidFor: {
none: {},
},
},
})

Copilot uses AI. Check for mistakes.
}

// Delete the group from database
await deleteGroupWithDocuments(groupId, false)
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleteDocuments parameter is passed to the function but never used. Line 20 hardcodes false, which means documents will never be deleted from S3 even when deleteDocuments is true. This appears to be a logic error where the intention was to pass deleteDocuments instead of false.

The call should be: await deleteGroupWithDocuments(groupId, deleteDocuments ?? false) to respect the user's choice.

Suggested change
await deleteGroupWithDocuments(groupId, false)
await deleteGroupWithDocuments(groupId, deleteDocuments ?? false)

Copilot uses AI. Check for mistakes.
Comment thread src/app/groups/json/import/route.ts Outdated
Comment on lines +92 to +94
await prisma.$transaction(async (tx) => {
await restoreGroupFromJSON(tx, jsonData, mode)
}, { timeout: 60000, maxWait: 20000 })
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transaction timeout is set to 60 seconds with a maxWait of 20 seconds for large imports. However, given the sequential nature of the operations in restoreGroupFromJSON (see performance issue in json-import.ts), this timeout may not be sufficient for very large groups (e.g., 500+ expenses). The maxWait of 20 seconds might cause the transaction to fail before it even starts if there's database contention.

Consider either: 1) Optimizing the restore operations to use batch inserts, 2) Increasing the timeout further, or 3) Adding a progress indicator and allowing the operation to be performed outside a transaction with proper error handling and rollback capabilities.

Copilot uses AI. Check for mistakes.
Comment thread src/app/groups/json/import/route.ts Outdated
Comment on lines +22 to +25
// Read the JSON file
const text = await file.text()
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const jsonData: JSONImportData = JSON.parse(text) as unknown as JSONImportData
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON parsing is vulnerable to malicious or malformed JSON files that could cause a denial of service. There's no file size validation before calling file.text(), which means an attacker could upload a multi-gigabyte JSON file that would consume server memory. Additionally, JSON.parse() has no protection against deeply nested objects that could cause stack overflow.

Add file size validation (e.g., max 10MB for reasonable group exports) before parsing, and consider using a streaming JSON parser for large files or adding recursion depth limits.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/backup.ts Outdated
Comment on lines +250 to +269
await prisma.expense.create({
data: {
id: expense.id,
groupId: group.id,
expenseDate: new Date(expense.expenseDate),
createdAt: new Date(expense.createdAt),
title: expense.title,
categoryId: expense.category?.id ?? 0,
amount: expense.amount,
originalAmount: expense.originalAmount,
originalCurrency: expense.originalCurrency,
conversionRate: expense.conversionRate ? new Prisma.Decimal(expense.conversionRate) : null,
paidById: expense.paidById,
isReimbursement: expense.isReimbursement,
splitMode: expense.splitMode as any,
notes: expense.notes,
recurrenceRule: expense.recurrenceRule as any,
},
})

Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backup restoration doesn't validate that categoryId references in expenses actually exist in the database. If a backup contains a categoryId that doesn't exist (or was deleted), the expense creation will fail with a foreign key constraint violation, causing the entire restore transaction to fail.

Consider either: 1) Pre-validate all category IDs and create missing categories from the backup data, 2) Use category information from the backup to recreate categories if they don't exist, or 3) Handle the foreign key error gracefully and default to categoryId: 0.

Suggested change
await prisma.expense.create({
data: {
id: expense.id,
groupId: group.id,
expenseDate: new Date(expense.expenseDate),
createdAt: new Date(expense.createdAt),
title: expense.title,
categoryId: expense.category?.id ?? 0,
amount: expense.amount,
originalAmount: expense.originalAmount,
originalCurrency: expense.originalCurrency,
conversionRate: expense.conversionRate ? new Prisma.Decimal(expense.conversionRate) : null,
paidById: expense.paidById,
isReimbursement: expense.isReimbursement,
splitMode: expense.splitMode as any,
notes: expense.notes,
recurrenceRule: expense.recurrenceRule as any,
},
})
const expenseData = {
id: expense.id,
groupId: group.id,
expenseDate: new Date(expense.expenseDate),
createdAt: new Date(expense.createdAt),
title: expense.title,
categoryId: expense.category?.id ?? 0,
amount: expense.amount,
originalAmount: expense.originalAmount,
originalCurrency: expense.originalCurrency,
conversionRate: expense.conversionRate ? new Prisma.Decimal(expense.conversionRate) : null,
paidById: expense.paidById,
isReimbursement: expense.isReimbursement,
splitMode: expense.splitMode as any,
notes: expense.notes,
recurrenceRule: expense.recurrenceRule as any,
}
try {
await prisma.expense.create({
data: expenseData,
})
} catch (err: any) {
// Handle potential foreign key constraint violations on categoryId
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2003') {
// Retry with a default/uncategorized categoryId
await prisma.expense.create({
data: {
...expenseData,
categoryId: 0,
},
})
warnings.push(
`Category not found for expense "${expense.title}", defaulted categoryId to 0 during restore.`,
)
} else {
throw err
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +123
export async function POST(req: Request) {
try {
const formData = await req.formData()
const file = formData.get('file') as File
const action = formData.get('action') as string // 'analyze' | 'restore' | 'rollback'

if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
}

// Read the JSON file
const text = await file.text()
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const jsonData: JSONImportData = JSON.parse(text) as unknown as JSONImportData

// Validate JSON data
if (!jsonData.id || !jsonData.name || !jsonData.participants || !jsonData.expenses) {
return NextResponse.json(
{ error: 'Invalid JSON file format. Expected Spliit export format with id, name, participants, and expenses.' },
{ status: 400 }
)
}

// Check if group exists
const existingGroup = await prisma.group.findUnique({
where: { id: jsonData.id },
include: {
participants: { select: { id: true } },
expenses: { select: { id: true, createdAt: true, expenseDate: true } },
activities: { select: { time: true } },
},
})

const comparison = compareJSONVersions(jsonData, existingGroup)

// If action is 'analyze', just return the comparison
if (action === 'analyze') {
let differences
if (existingGroup) {
differences = calculateJSONDifferences(jsonData, existingGroup)
}

return NextResponse.json({
success: true,
comparison: {
result: comparison.result,
existingGroupUpdatedAt: comparison.existingGroupUpdatedAt?.toISOString(),
jsonExportedAt: comparison.jsonExportedAt.toISOString(),
differences,
},
groupName: jsonData.name,
warnings: [
'JSON import has limitations:',
'• Activity history is not preserved, it will be regenerated',
'• Document attachments will not be imported',
'• Notes on expenses will not be imported',
'• Recurring expense links will not be imported',
'• Only basic expense data will be restored',
],
})
}

// Handle restore/rollback actions
if (action === 'restore' || action === 'rollback') {
let mode: 'create' | 'update' | 'rollback'

if (comparison.result === VersionComparisonResult.NOT_FOUND) {
mode = 'create'
} else if (action === 'rollback') {
mode = 'rollback'
} else if (comparison.result === VersionComparisonResult.NEWER) {
mode = 'update'
} else {
return NextResponse.json(
{ error: 'JSON export is not newer than existing group. Use rollback to restore older version.' },
{ status: 400 }
)
}

// Execute restore in a transaction (increase timeout for large imports)
await prisma.$transaction(async (tx) => {
await restoreGroupFromJSON(tx, jsonData, mode)
}, { timeout: 60000, maxWait: 20000 })
// Revalidate the group pages to ensure fresh data is loaded
revalidatePath(`/groups/${jsonData.id}`)
revalidatePath(`/groups/${jsonData.id}/expenses`)
revalidatePath(`/groups/${jsonData.id}/balances`)
revalidatePath('/groups')

return NextResponse.json({
success: true,
message: `Group ${mode === 'create' ? 'created' : mode === 'rollback' ? 'rolled back' : 'updated'} successfully`,
groupId: jsonData.id,
mode,
})
}

return NextResponse.json(
{ error: 'Invalid action. Use "analyze", "restore", or "rollback"' },
{ status: 400 }
)
} catch (error) {
console.error('JSON import error:', error)
return NextResponse.json(
{
error: 'Failed to process JSON file',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
)
}
}
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These API routes lack any authentication or authorization checks. Anyone who knows a groupId can restore a backup, import JSON data, or delete all imported data for that group. This is a critical security vulnerability as it allows unauthorized access to modify or destroy group data.

At minimum, implement validation that the requester has permission to modify the group (e.g., by checking if they've recently accessed it, or by implementing a proper authentication system). Consider requiring a confirmation token or password for destructive operations.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +111
export async function POST(
req: Request,
{ params }: { params: Promise<{ groupId: string }> }
) {
try {
const { groupId } = await params
const { action } = await req.json() as { action: 'undo-import' }

// Verify group exists
const group = await prisma.group.findUnique({
where: { id: groupId },
include: {
activities: {
where: {
data: {
startsWith: 'JSON_IMPORT_START:',
},
},
orderBy: { time: 'desc' },
take: 1,
},
},
})

if (!group) {
return NextResponse.json(
{ error: 'Group not found' },
{ status: 404 }
)
}

if (action === 'undo-import') {
// Find the most recent import marker
const lastImport = group.activities[0]

if (!lastImport) {
return NextResponse.json(
{ error: 'No import found to undo' },
{ status: 400 }
)
}

// Delete all expenses and participants created after the import marker
await prisma.$transaction(async (tx) => {
// Delete expense paid-for entries first (foreign key constraint)
await tx.expensePaidFor.deleteMany({
where: {
expense: {
groupId: groupId,
createdAt: {
gte: lastImport.time,
},
},
},
})

// Delete expenses created during or after import
await tx.expense.deleteMany({
where: {
groupId: groupId,
createdAt: {
gte: lastImport.time,
},
},
})

// Delete participants - Note: Can't filter by createdAt as it doesn't exist
// We'll keep participants to avoid breaking expense references
// Alternatively, delete only participants not referenced by remaining expenses

// Delete activities from the import
await tx.activity.deleteMany({
where: {
groupId: groupId,
time: {
gte: lastImport.time,
},
},
})
})

revalidatePath(`/groups/${groupId}`)
revalidatePath(`/groups/${groupId}/expenses`)
revalidatePath(`/groups/${groupId}/balances`)
revalidatePath(`/groups/${groupId}/activity`)

return NextResponse.json({
success: true,
message: 'Successfully undid last import',
})
}

return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
} catch (error) {
console.error('Delete operation error:', error)
return NextResponse.json(
{
error: 'Failed to delete data',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
)
}
}
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This route allows anyone to delete all imported data from a group without authentication or authorization. This is a critical security vulnerability that could allow malicious actors to destroy data.

Implement authentication/authorization checks to ensure the requester has permission to delete data from the group.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +151
// Show warnings if any documents were missing
if (data.warnings && data.warnings.length > 0) {
const warningMessage = [
'Backup restored successfully, but some issues were found:',
'',
...data.warnings,
'',
'The group was restored without these missing documents.',
].join('\n')

// eslint-disable-next-line no-alert
alert(warningMessage)
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using native alert() for displaying warnings is not user-friendly and breaks the modern UI/UX pattern of the application. The alert is modal and blocks the UI, and on some mobile browsers may not display properly. Additionally, the warnings could contain long lists of missing documents that won't fit well in an alert dialog.

Consider using a toast notification, dialog component, or dedicated warnings section in the UI to display these messages in a more user-friendly way that matches the rest of the application's design.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +66
const parsed = JSON.parse(activity.data) as { title?: string; importDate?: string }
if (typeof parsed.title === 'string') expenseTitle = parsed.title
if (typeof parsed.importDate === 'string') importDate = parsed.importDate
} catch {
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON parsing of activity.data (line 63) lacks proper error handling and validation. While there's a try-catch block, it silently fails without logging, making it difficult to debug issues. Additionally, there's no validation that the parsed object has the expected structure before accessing its properties.

Consider adding validation for the parsed object structure and logging parse errors in development mode to help with debugging.

Suggested change
const parsed = JSON.parse(activity.data) as { title?: string; importDate?: string }
if (typeof parsed.title === 'string') expenseTitle = parsed.title
if (typeof parsed.importDate === 'string') importDate = parsed.importDate
} catch {
const parsed = JSON.parse(activity.data)
if (parsed && typeof parsed === 'object') {
const { title, importDate: parsedImportDate } = parsed as {
title?: unknown
importDate?: unknown
}
if (typeof title === 'string') expenseTitle = title
if (typeof parsedImportDate === 'string') importDate = parsedImportDate
}
} catch (error) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.error('Failed to parse activity.data JSON', { error, data: activity.data })
}

Copilot uses AI. Check for mistakes.
Comment thread src/lib/json-import.ts Outdated
jsonData: JSONImportData,
existingGroup: ExistingGroup,
): JSONGroupComparison {
const existingExpenseIds = new Set(existingGroup.expenses.map(e => e.id))
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable existingExpenseIds.

Suggested change
const existingExpenseIds = new Set(existingGroup.expenses.map(e => e.id))

Copilot uses AI. Check for mistakes.
@carnach
Copy link
Copy Markdown
Owner Author

carnach commented Feb 1, 2026

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

Copilot AI commented Feb 1, 2026

@carnach I've opened a new pull request, #15, to work on those changes. Once the pull request is ready, I'll request review from you.

…nts (#15)

* Initial plan

* fix: address simple review comments - participant cleanup, file validation, S3 extraction

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix: improve error handling, logging, and internationalization

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* perf: optimize backup and JSON import with batch database operations

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix: improve undo import logic and replace alert with toast

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix: ensure categories exist before restoring expenses from backup

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* docs: add security notes about link-sharing authentication model

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix: use safe delimiter for category keys and remove unused variable

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* refactor: simplify prisma query and improve comments

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* refactor: extract category key helpers and remove sensitive data from logs

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix: add validation to category key parser and sanitize error logs

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* match prisma schema

* dded the downlevelIteration compiler option to tsconfig.json

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>
Co-authored-by: carnach <gevert@gmail.com>
@carnach carnach changed the base branch from main to release February 1, 2026 09:29
@carnach carnach merged commit 205511a into release Feb 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants