refactor(time-tracker): time tracking proof images now uses signed URLs#4344
refactor(time-tracker): time tracking proof images now uses signed URLs#4344
Conversation
…tor request handling Added functionality to upload time tracking images using signed URLs, enhancing the request mutation process. Refactored the request handling to accept JSON payloads instead of FormData, improving API consistency and error handling. Updated related components to support the new image upload mechanism.
Updated the time tracking repository and related components to use imageLocalPaths instead of imagePaths for better clarity. Implemented a new image upload flow using signed URLs, enhancing the request creation and update processes. Added unit tests to ensure the functionality of the new upload mechanism.
…request submission feedback
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
2 issues found across 41 files
Confidence score: 3/5
- Potential authorization gap in
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tscould let workspace members generate upload URLs for arbitrary request IDs, which is a concrete access-control risk. - Edit validation in
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/sessions/[sessionId]/actions/edit.tscan allow invalid time intervals when only one bound is updated, which could lead to inconsistent session data. - These are medium-severity, user-impacting issues, so while the PR may be close, it carries some merge risk that should be addressed.
- Pay close attention to
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsandapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/sessions/[sessionId]/actions/edit.ts- authorization and time-range validation gaps.
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/sessions/[sessionId]/actions/edit.ts">
<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/sessions/[sessionId]/actions/edit.ts:52">
P2: Validation only checks start < end when both startTime and endTime are supplied. Updating just one field can create an invalid interval (e.g., new start after existing end). Validate against the existing session time when only one field is provided.</violation>
</file>
<file name="apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.ts">
<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.ts:93">
P1: Validate that requestId belongs to the current workspace/user before generating signed upload URLs. Without this check, any workspace member can request upload URLs for arbitrary request IDs and write into other users’ request folders.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Summary of ChangesHello @VNOsST, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a more secure and efficient way to handle image uploads for time tracking requests by leveraging signed URLs. It also significantly enhances the time tracking history view with period-based navigation and detailed statistics. Furthermore, the user experience for editing existing sessions and adding missed entries has been improved with clearer validation rules and immediate feedback, ensuring data integrity and compliance with workspace settings. Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
🔧 Biome Check ReportTotal Issues Found: 0🔧 Format Check Results ✅
🔍 Lint Check Results ✅
🎉 All Issues Resolved!Your code is now 100% clean! Great job! 🏆
🤖 Auto-generated by Biome Check workflow • Last updated: 2/24/2026, 9:11:19 AM
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4344 +/- ##
==========================================
- Coverage 49.11% 48.99% -0.13%
==========================================
Files 256 257 +1
Lines 19633 19732 +99
Branches 6706 6777 +71
==========================================
+ Hits 9643 9667 +24
- Misses 8141 8184 +43
- Partials 1849 1881 +32 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request introduces several changes to the time tracking feature, including new data models for period statistics and session pages, modifications to the time tracker repository to support fetching historical sessions and period stats, and UI enhancements in the shell page and time tracker page. The changes also include image uploading using signed URLs, and various bug fixes and improvements. Review comments suggest scoping the admin delete operation to also include the user_id and removing the unused permissions check, using the uuid package instead of manual implementation, and wrapping the dialog's content in a WillPopScope widget to block dismissal while _isSubmitting is true.
I am having trouble creating individual review comments. Click here to see my feedback.
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/sessions/[sessionId]/route.ts (196-209)
The DELETE handler correctly verifies session ownership using the user's Supabase client. However, the actual deletion is performed with the admin client and is only scoped by sessionId and wsId, not the user ID. This could lead to accidental deletion if the ownership check logic were to have a bug. Additionally, permissions are fetched but not used, which is dead code.
To improve security and clarity, I recommend scoping the admin delete operation to also include the user_id and removing the unused permissions check. This ensures that only the verified owner's session can be deleted.
const sbAdmin = await createAdminClient();
const { error } = await sbAdmin
.from('time_tracking_sessions')
.delete()
.match({ id: sessionId, ws_id: normalizedWsId, user_id: user.id });apps/mobile/lib/data/repositories/time_tracker_repository.dart (234-251)
While this manual implementation of _generateUuidV4 appears correct, it's generally better to use a standard, well-tested package for generating UUIDs to ensure correctness and reduce boilerplate code. Consider using the uuid package. You would need to add it to your pubspec.yaml and import it.
String _generateUuidV4() {
const uuid = Uuid();
return uuid.v4();
}apps/mobile/lib/features/time_tracker/widgets/edit_session_dialog.dart (77-287)
The dialog now handles async save operations, which is great. However, the user can still dismiss the dialog (e.g., by tapping the back button or scrim) while the save operation is in progress. This can lead to a confusing user experience. To prevent this, consider wrapping the dialog's content in a WillPopScope widget to block dismissal while _isSubmitting is true.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.ts (1)
249-332:⚠️ Potential issue | 🟠 MajorCapture storage deletion errors before updating the database.
Theremove(removedImages)call at line 303–305 ignores its error result. If deletion fails, the database is updated and the images are removed from the records while the files remain in storage, causing orphaned data. Also applies to the cleanupremove()call at line 329–331.Example fix
- await storageClient.storage - .from('time_tracking_requests') - .remove(removedImages); + const { error: removeError } = await storageClient.storage + .from('time_tracking_requests') + .remove(removedImages); + if (removeError) { + console.error('Failed to remove images:', removeError); + return NextResponse.json( + { error: 'Failed to remove images from storage' }, + { status: 500 } + ); + }Apply the same pattern to the cleanup removal at line 329–331.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/api/v1/workspaces/`[wsId]/time-tracking/requests/[id]/route.ts around lines 249 - 332, The storage deletion calls currently ignore their results—capture and check the result of storageClient.storage.from('time_tracking_requests').remove when deleting removedImages before performing the DB update and return a 500 (or handle appropriately) if the removal fails to avoid leaving orphaned files; likewise, when cleaning up newly uploaded images after an update error (the remove(newImagePaths) call guarded by updateError), also check the removal result and surface/log/return an error if cleanup fails so failures are not silently ignored. Locate the calls that reference removedImages/newImagePaths, storageClient.storage.from('time_tracking_requests').remove, and the update error handling around updateError to implement this change.packages/hooks/src/hooks/use-missed-entry-form.ts (1)
365-474:⚠️ Potential issue | 🟠 MajorRefactor mutations to use TanStack Query
useMutationinstead of rawfetch()calls.This custom hook uses raw
fetch()for mutations increateMissedEntryandhandleDiscardSessionfunctions. While the hook does callqueryClient.invalidateQueries()afterward, this violates the mandatory requirement to use TanStack Query hooks for all client-side data fetching.Wrap these mutations in
useMutation()hooks and ensure fetch calls withinqueryFnincludecache: 'no-store'to prevent dual-layer caching conflicts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/hooks/src/hooks/use-missed-entry-form.ts` around lines 365 - 474, The createMissedEntry (and related handleDiscardSession) functions perform POST/PATCH/DELETE via raw fetch() — refactor these mutation calls to use TanStack Query's useMutation hooks instead: extract the network logic currently inside createMissedEntry (the PATCH to sessions, DELETE session, image upload POST, and the POST to /time-tracking/requests) into mutation queryFn(s) wired with useMutation, call mutate/mutateAsync from createMissedEntry/handleDiscardSession, and ensure each fetch in those queryFn functions includes { cache: 'no-store' } in the request options; keep existing error handling and call queryClient.invalidateQueries() in onSuccess/onSettled of the mutations rather than after raw fetches.apps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dart (1)
114-152:⚠️ Potential issue | 🟠 MajorRethrow updateRequest failures after emitting error state.
updateRequestswallows exceptions by returningnull, which prevents callers from handling failures consistently. Emit the error state and rethrow to comply with the Cubit error-handling rule.🔧 Suggested fix
} on Exception catch (e) { emit(state.copyWith(error: e.toString())); - return null; + rethrow; }As per coding guidelines: “apps/mobile/lib/**/*{cubit,bloc}.dart: If a Flutter Cubit catches repository errors, rethrow after emitting error state so callers can handle failures locally.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dart` around lines 114 - 152, The updateRequest method currently catches exceptions, emits an error state, and returns null which hides failures; modify the catch block in updateRequest (the try around _repo.updateRequest and emit via state.copyWith) so that after emitting the error state you rethrow the exception (use rethrow to preserve stack) instead of returning null, ensuring callers can handle failures while the Cubit still updates its state.apps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dart (1)
303-370:⚠️ Potential issue | 🟡 MinorCapture root navigator before the first await.
Navigator.of(context, rootNavigator: true)is accessed afterawait widget.onSave(...). Capture it up front to avoid context reads after the first await.🔧 Suggested fix
- final navigator = Navigator.of(context); + final navigator = Navigator.of(context); + final rootNavigator = Navigator.of( + context, + rootNavigator: true, + ); setState(() => _isSubmitting = true); ... - final toastContext = Navigator.of( - context, - rootNavigator: true, - ).context; + final toastContext = rootNavigator.context; ... - final toastContext = Navigator.of( - context, - rootNavigator: true, - ).context; + final toastContext = rootNavigator.context;As per coding guidelines: “Capture
contextdependencies before firstawaitand guardBuildContextusage afterawaitwithif (!context.mounted) return;in Flutter.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dart` around lines 303 - 370, The code reads Navigator.of(context, rootNavigator: true).context after awaiting widget.onSave which risks using a stale BuildContext; capture that root navigator context before the first await and reuse it afterwards. Specifically, before calling await widget.onSave(...) compute a final variable (e.g. toastContext = Navigator.of(context, rootNavigator: true).context) and use that variable in subsequent shad.showToast calls instead of calling Navigator.of(...) again; keep existing checks for if (!context.mounted) after the await and maintain setting _isSubmitting and dialog/navigator behavior as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/mobile/test/data/repositories/time_tracker_repository_test.dart`:
- Around line 40-192: The temp directories created in the two tests
('createRequest uploads images first and sends storage paths' and 'updateRequest
uploads new images and sends JSON body') are deleted manually at the end and
won’t be removed if an assertion fails; after creating tempDir (the Directory
returned by Directory.systemTemp.createTemp) register a tear-down that deletes
it (use addTearDown(() async => await tempDir.delete(recursive: true))); remove
the manual await tempDir.delete(...) calls at the end of each test so cleanup
always runs even if repository.createRequest or repository.updateRequest
assertions fail.
In
`@apps/web/src/app/`[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.ts:
- Around line 216-277: The local uploadTimeTrackingImages function duplicates
existing logic in packages/hooks/src/upload-time-tracking-images.ts; remove this
local implementation and import the shared helper instead (ensure the helper is
exported from the hooks package index if it isn't already). Edit the module that
declares uploadTimeTrackingImages to import the shared function (same name or
alias) from the hooks package and replace all internal calls/usages with that
import; if the helper isn't exported from the package root, add a re-export in
the hooks package index so it can be consumed here.
In
`@apps/web/src/app/api/v1/workspaces/`[wsId]/time-tracking/requests/upload-url/route.ts:
- Around line 16-33: The batch upload endpoint allows up to
MAX_FILES_PER_REQUEST (currently 10) which diverges from the create route limit
of 5 and can cause orphaned uploads; change MAX_FILES_PER_REQUEST to 5 and
ensure BatchUploadUrlRequestSchema (the files array validation) uses that
constant so the upload-url route enforces the same cap as the create route
(review SingleUploadUrlRequestSchema and UploadUrlFileSchema references to
confirm no other limits must be adjusted).
In `@packages/hooks/src/hooks/use-missed-entry-form.ts`:
- Line 8: The import in use-missed-entry-form.ts uses a deep relative path to
uploadTimeTrackingImages; re-export uploadTimeTrackingImages from the package
root index (add it to the package's src/index.ts exports) and update the import
in use-missed-entry-form.ts to import { uploadTimeTrackingImages } from the
package root (i.e., from the package name) instead of
'../upload-time-tracking-images' so it conforms to the package's public API
rule.
---
Outside diff comments:
In
`@apps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dart`:
- Around line 114-152: The updateRequest method currently catches exceptions,
emits an error state, and returns null which hides failures; modify the catch
block in updateRequest (the try around _repo.updateRequest and emit via
state.copyWith) so that after emitting the error state you rethrow the exception
(use rethrow to preserve stack) instead of returning null, ensuring callers can
handle failures while the Cubit still updates its state.
In `@apps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dart`:
- Around line 303-370: The code reads Navigator.of(context, rootNavigator:
true).context after awaiting widget.onSave which risks using a stale
BuildContext; capture that root navigator context before the first await and
reuse it afterwards. Specifically, before calling await widget.onSave(...)
compute a final variable (e.g. toastContext = Navigator.of(context,
rootNavigator: true).context) and use that variable in subsequent shad.showToast
calls instead of calling Navigator.of(...) again; keep existing checks for if
(!context.mounted) after the await and maintain setting _isSubmitting and
dialog/navigator behavior as-is.
In
`@apps/web/src/app/api/v1/workspaces/`[wsId]/time-tracking/requests/[id]/route.ts:
- Around line 249-332: The storage deletion calls currently ignore their
results—capture and check the result of
storageClient.storage.from('time_tracking_requests').remove when deleting
removedImages before performing the DB update and return a 500 (or handle
appropriately) if the removal fails to avoid leaving orphaned files; likewise,
when cleaning up newly uploaded images after an update error (the
remove(newImagePaths) call guarded by updateError), also check the removal
result and surface/log/return an error if cleanup fails so failures are not
silently ignored. Locate the calls that reference removedImages/newImagePaths,
storageClient.storage.from('time_tracking_requests').remove, and the update
error handling around updateError to implement this change.
In `@packages/hooks/src/hooks/use-missed-entry-form.ts`:
- Around line 365-474: The createMissedEntry (and related handleDiscardSession)
functions perform POST/PATCH/DELETE via raw fetch() — refactor these mutation
calls to use TanStack Query's useMutation hooks instead: extract the network
logic currently inside createMissedEntry (the PATCH to sessions, DELETE session,
image upload POST, and the POST to /time-tracking/requests) into mutation
queryFn(s) wired with useMutation, call mutate/mutateAsync from
createMissedEntry/handleDiscardSession, and ensure each fetch in those queryFn
functions includes { cache: 'no-store' } in the request options; keep existing
error handling and call queryClient.invalidateQueries() in onSuccess/onSettled
of the mutations rather than after raw fetches.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (3)
apps/mobile/lib/l10n/gen/app_localizations.dartis excluded by!**/gen/**apps/mobile/lib/l10n/gen/app_localizations_en.dartis excluded by!**/gen/**apps/mobile/lib/l10n/gen/app_localizations_vi.dartis excluded by!**/gen/**
📒 Files selected for processing (17)
apps/mobile/lib/data/repositories/time_tracker_repository.dartapps/mobile/lib/features/time_tracker/cubit/time_tracker_cubit.dartapps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dartapps/mobile/lib/features/time_tracker/view/time_tracker_requests_page.dartapps/mobile/lib/features/time_tracker/widgets/edit_request_dialog.dartapps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dartapps/mobile/lib/features/time_tracker/widgets/request_detail_sheet.dartapps/mobile/lib/features/time_tracker/widgets/timer_tab.dartapps/mobile/lib/l10n/arb/app_en.arbapps/mobile/lib/l10n/arb/app_vi.arbapps/mobile/test/data/repositories/time_tracker_repository_test.dartapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tspackages/hooks/src/hooks/use-missed-entry-form.tspackages/hooks/src/upload-time-tracking-images.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (20)
apps/mobile/lib/**/*.dart
📄 CodeRabbit inference engine (CLAUDE.md)
apps/mobile/lib/**/*.dart: Useflutter_blocwith Cubits for feature state management in Flutter app
Usego_routerwith auth-aware redirects and ShellRoute for bottom tabs in Flutter app
Usesupabase_flutterwithflutter_secure_storagefor token persistence in Flutter app
Followvery_good_analysislinting rules in Flutter app
Useon Exception catch (e)instead of barecatchin Flutter code; avoid catchingErrorsubclasses
Capturecontextdependencies before firstawaitand guardBuildContextusage afterawaitwithif (!context.mounted) return;in Flutter
Never return from afinallyblock in Dart/Flutter code
UseFuture<void> Function()callbacks for mutation-driven UI actions in Flutter; await before closing dialogs
apps/mobile/lib/**/*.dart: When refactoring duplicated Flutter editable fields into shared widgets, preserve per-field validation and success messaging. Email fields should keep the@check and any email-specific success note (useTextInputType.emailAddressor an explicit parameter).
Useon Exception catch (e)(or specific exception types) instead of barecatch, avoid catchingErrorsubclasses likeTypeError, capturecontextdependencies before the firstawait, guardBuildContextusage afterawaitwithif (!context.mounted) return;, and do notreturninsidefinallyblocks.
The Flutter mobile app uses BLoC/Cubit state management,go_routernavigation, andsupabase_flutterfor auth. Build flavors:main_development.dart,main_staging.dart,main_production.dart.
For mutation-driven Flutter UI actions (approve/reject/update), useFuture<void> Function()callbacks (notVoidCallback), await them before closing dialogs/sheets, and surface failures in the UI (e.g.,SnackBar).
apps/mobile/lib/**/*.dart: Useon Exception catch (e)with specific exception types in Flutter; avoid untypedcatchblocks
Checkcontext.mountedbefore usingBuildContextafterawaitin Flutter to preven...
Files:
apps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dartapps/mobile/lib/features/time_tracker/widgets/timer_tab.dartapps/mobile/lib/features/time_tracker/widgets/edit_request_dialog.dartapps/mobile/lib/features/time_tracker/widgets/request_detail_sheet.dartapps/mobile/lib/features/time_tracker/cubit/time_tracker_cubit.dartapps/mobile/lib/features/time_tracker/view/time_tracker_requests_page.dartapps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dartapps/mobile/lib/data/repositories/time_tracker_repository.dart
apps/mobile/**/*.dart
📄 CodeRabbit inference engine (GEMINI.md)
Always run
bun check:mobileafter changes toapps/mobile/. This runsdart format --set-exit-if-changed lib test && flutter analyze && flutter test. All three checks MUST pass. If it reports a Dart format failure, rerun to confirm a clean pass.
apps/mobile/**/*.dart: Rundart format --set-exit-if-changed lib testinapps/mobile/to verify code formatting; fix withdart format lib test
Runbun check:mobile(dart format + flutter analyze + flutter test) after making changes toapps/mobile/
Files:
apps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dartapps/mobile/lib/features/time_tracker/widgets/timer_tab.dartapps/mobile/lib/features/time_tracker/widgets/edit_request_dialog.dartapps/mobile/lib/features/time_tracker/widgets/request_detail_sheet.dartapps/mobile/lib/features/time_tracker/cubit/time_tracker_cubit.dartapps/mobile/test/data/repositories/time_tracker_repository_test.dartapps/mobile/lib/features/time_tracker/view/time_tracker_requests_page.dartapps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dartapps/mobile/lib/data/repositories/time_tracker_repository.dart
apps/mobile/lib/**/*{cubit,bloc}.dart
📄 CodeRabbit inference engine (GEMINI.md)
If a Flutter Cubit catches repository errors, rethrow after emitting error state so callers can handle failures locally.
Files:
apps/mobile/lib/features/time_tracker/cubit/time_tracker_requests_cubit.dartapps/mobile/lib/features/time_tracker/cubit/time_tracker_cubit.dart
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript with explicit return types for exported functions
Prefer interface for defining object shapes in TypeScript
Use TanStack Query (React Query) for ALL client-side data fetching - NEVER use useEffect for data fetching
Always includecache: 'no-store'in fetch() calls within TanStack Query queryFn to prevent dual-layer caching conflicts
Use Server Components by default; add'use client'only when necessary
Use@tuturuuu/ui/sonnerfor toast notifications instead of the deprecated@tuturuuu/ui/toast
Refactor files exceeding 400 LOC and components exceeding 200 LOC into smaller, focused units
Extract complex logic to separate utilities and custom hooks following single responsibility principle
Import database types frompackages/types/src/db.tsinstead of manually defining database types (only after user runs migrations viabun sb:pushand typegen viabun sb:typegen)
Always use stable array query keys with structure:[domain, subdomain?, paramsHash, version?]in TanStack Query
Use discriminated unions over enums for TypeScript type definitions
Narrowunknown/anyat boundaries; justify with comments if needed
Querypublic.user_private_detailstable for user email addresses instead ofpublic.userstable
Reference environment variables by name only in code - never echo values
Preferinterfaceovertypefor defining object shapes in TypeScript
Use camelCase for variable and function names in TypeScript/JavaScript
Use SCREAMING_SNAKE_CASE for constants
**/*.{ts,tsx}: Always prefer importing database types frompackages/types/src/db.ts. Never attempt to run migrations yourself - only after user runsbun sb:pushandbun sb:typegen.
Thepublic.userstable does NOT contain anpublic.user_private_details. When querying user email, always useuser_private_detailstable, notusers.
**/*.{ts,tsx}: Preferinterfacefor defining object shapes in TypeScript
N...
Files:
packages/hooks/src/upload-time-tracking-images.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tspackages/hooks/src/hooks/use-missed-entry-form.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
packages/**/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid deep relative imports; expose via package
index.ts
Files:
packages/hooks/src/upload-time-tracking-images.tspackages/hooks/src/hooks/use-missed-entry-form.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (GEMINI.md)
Always use
bun type-checkcommand for type checking. Do NOT usenpx tsgo,bunx tsgo, or other alternatives.
Files:
packages/hooks/src/upload-time-tracking-images.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tspackages/hooks/src/hooks/use-missed-entry-form.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
**/*.{ts,tsx,js,jsx,json,yaml,yml}
📄 CodeRabbit inference engine (GEMINI.md)
Use Biome for linting and formatting. Run
bun format-and-lint:fixto automatically fix issues.
Files:
packages/hooks/src/upload-time-tracking-images.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tspackages/hooks/src/hooks/use-missed-entry-form.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
**/{src,lib}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
Files >400 LOC and components >200 LOC should be refactored into smaller, focused units. Apply best practices to ALL code, regardless of age. Follow single responsibility principle, extract utilities/hooks for complex logic, and leave code better than you found it.
Files:
packages/hooks/src/upload-time-tracking-images.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tspackages/hooks/src/hooks/use-missed-entry-form.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
**/*.ts: Avoid usinganytype in TypeScript; narrowunknownat boundaries with explicit type guards
Use discriminated unions over enums unless runtime enum is required
Always usebun type-checkfor TypeScript type checking; do NOT usenpx tsgo,bunx tsgo, or other alternatives
Files:
packages/hooks/src/upload-time-tracking-images.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tspackages/hooks/src/hooks/use-missed-entry-form.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/*/src/app/api/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
apps/*/src/app/api/**/*.ts: Use Zod for runtime validation of external inputs at API boundaries
Validate inputs early in API routes and return 400/401/403 for bad requests
Never leak stack traces to client; wrap external service calls and surface sanitized error messages
Log detailed errors server-side only; never expose sensitive information to client
Define and export runtime if edge is required:export const runtime = 'edge'
Use@tuturuuu/supabaseclient for authentication in API routes
UsenormalizeWorkspaceId()helper to resolve workspace ID parameters in API routes
Enforce auth on AI endpoints by getting user viacreateClient()and returning 401 if absent
Check feature flags inworkspace_secretsbefore serving AI features
Use Vercel AI SDKstreamObject/generateObjectwith selected model for AI operations
SetmaxDurationfor long-running AI operations
Never log secrets or raw provider responses in AI endpoints
apps/*/src/app/api/**/*.ts: UsecreateClient(request)pattern in API routes to support both Bearer token auth (mobile) and cookie auth (web)
Useexport const runtime = 'edge'to explicitly target edge runtime execution in Next.js API routes when appropriate
Use Zod for runtime validation of external inputs (API bodies, env-derived config) at system boundaries
Resolve workspace ID parameters (wsId) to UUIDs before database operations; usenormalizeWorkspaceId()for special identifiers like 'personal' and 'internal'
Validate inputs early in API routes; return 4xx errors before processing to fail fast
Addexport const runtime = 'edge'for routes that should execute on the edge runtime
Use Vercel AI SDKgenerateObject/streamObjectwith Zod schema for deterministic AI output structure
UsecreateAdminClient()(sbAdmin) when bulk operations need to bypass trigger permission checks (e.g., user merge); always validate workspace membership first
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/*/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/*/src/**/*.{ts,tsx}: Extract functions used ≥2 times tosrc/lib/orsrc/utils/
Extract complex state logic to custom hooks insrc/hooks/
Extract pure computations as testable utilities
apps/*/src/**/*.{ts,tsx}: Never log secrets, API keys, tokens, or raw sensitive data to console; only reference environment variable names
Query thepublic.user_private_detailstable for user email addresses; thepublic.userstable does NOT contain an
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
apps/web/**/*.{ts,tsx}: Never useuseEffectfor data fetching. MANDATORY: Use TanStack Query (useQuery,useMutation,useInfiniteQuery) for all client-side data fetching.
Never use rawfetch()without TanStack Query wrapper in client components. Always use TanStack Query hooks for client-side data fetching.
Anyfetch()inside aqueryFnMUST includecache: 'no-store'to prevent browser HTTP cache from serving stale responses after TanStack Query invalidation.
Use stable array query keys following pattern:[domain, subdomain?, paramsHash, version?]. SetstaleTime> 0 for rarely-changing data. Implement optimistic updates with rollback. Use narrow invalidations, avoid globalinvalidateQueries().
Follow the Tailwind Dynamic Color Policy. Never use hard-coded color classes liketext-blue-500. Instead, use thedynamic-*tokens, e.g.,text-dynamic-blue.
Usesonnerfor toasts:import { toast } from '@tuturuuu/ui/sonner';. Avoid the deprecated@tuturuuu/ui/toast. Never use native browser dialogs likealert()orconfirm().
Use the dialog system for modals:import { Dialog, ... } from '@tuturuuu/ui/dialog';. Never use native browser dialogs.
Databasews_idcolumns ALWAYS store UUIDs. Route parameters (wsId) may contain special identifiers likepersonalorinternal. NEVER use rawwsIddirectly in database queries. UsenormalizeWorkspaceId(wsId)helper for API routes or acceptworkspaceprop in components.
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/web/src/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
When adding new pages or routes, ALWAYS update the main navigation file (
apps/web/src/app/[locale]/(dashboard)/[wsId]/navigation.tsx). Add routes to both thealiasesarray andchildrennavigation items with proper icons, permission checks, and translation keys in bothen.jsonANDvi.json.
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/*/src/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
Create new API routes in
apps/<app>/src/app/api/.... Use Supabase client wrappers for authentication and Zod for input validation.
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/web/src/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
The mobile app connects to
apps/webAPI routes via Bearer token auth (not cookies). When updating web API routes for mobile features, maintain backward compatibility and usecreateClient(request)for Bearer token auth.
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
Extract utilities to
src/lib/, hooks tosrc/hooks/, and sub-components as needed. Apply best practices to BOTH old and new code. Follow single responsibility principle, use meaningful names, and eliminate duplication.
Files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
apps/mobile/lib/l10n/arb/*.arb
📄 CodeRabbit inference engine (CLAUDE.md)
Maintain ARB localization files in
lib/l10n/arb/with both English and Vietnamese translations
Files:
apps/mobile/lib/l10n/arb/app_vi.arbapps/mobile/lib/l10n/arb/app_en.arb
apps/mobile/lib/l10n/**/*.{arb,dart}
📄 CodeRabbit inference engine (GEMINI.md)
When updating
apps/mobile/lib/l10n/arb/*.arb, regenerate or updateapps/mobile/lib/l10n/gen/*so tracked outputs stay in sync.
Files:
apps/mobile/lib/l10n/arb/app_vi.arbapps/mobile/lib/l10n/arb/app_en.arb
apps/mobile/test/**/*.dart
📄 CodeRabbit inference engine (CLAUDE.md)
Wrap Flutter widget tests rendering
shadcn_fluttercomponents withshad.ShadcnAppincludingshad.ShadcnLocalizations.delegateWrap Flutter widgets using
shadcn_flutterwithshad.ShadcnAppin tests to provide theme context
Files:
apps/mobile/test/data/repositories/time_tracker_repository_test.dart
apps/mobile/**/test/**/*.dart
📄 CodeRabbit inference engine (GEMINI.md)
Wrap widgets that use
shadcn_flutterinshad.ShadcnAppwithshad.ShadcnLocalizations.delegatesoshad.Theme.of(context)resolves in tests.
Files:
apps/mobile/test/data/repositories/time_tracker_repository_test.dart
🧠 Learnings (10)
📚 Learning: 2026-02-23T06:49:53.243Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: GEMINI.md:0-0
Timestamp: 2026-02-23T06:49:53.243Z
Learning: Applies to apps/mobile/lib/**/*.dart : For mutation-driven Flutter UI actions (approve/reject/update), use `Future<void> Function()` callbacks (not `VoidCallback`), await them before closing dialogs/sheets, and surface failures in the UI (e.g., `SnackBar`).
Applied to files:
apps/mobile/lib/features/time_tracker/widgets/timer_tab.dartapps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dart
📚 Learning: 2026-02-23T06:49:12.270Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-23T06:49:12.270Z
Learning: Applies to apps/*/src/app/api/**/*.ts : Use `normalizeWorkspaceId()` helper to resolve workspace ID parameters in API routes
Applied to files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
📚 Learning: 2026-02-23T06:49:53.243Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: GEMINI.md:0-0
Timestamp: 2026-02-23T06:49:53.243Z
Learning: Applies to apps/*/src/app/api/**/*.{ts,tsx} : Create new API routes in `apps/<app>/src/app/api/...`. Use Supabase client wrappers for authentication and Zod for input validation.
Applied to files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
📚 Learning: 2026-02-23T06:49:12.270Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-23T06:49:12.270Z
Learning: Applies to apps/*/src/app/api/**/*.ts : Use `tuturuuu/supabase` client for authentication in API routes
Applied to files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
📚 Learning: 2026-02-23T06:49:12.270Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-23T06:49:12.270Z
Learning: Applies to apps/mobile/lib/l10n/arb/*.arb : Maintain ARB localization files in `lib/l10n/arb/` with both English and Vietnamese translations
Applied to files:
apps/mobile/lib/l10n/arb/app_vi.arbapps/mobile/lib/l10n/arb/app_en.arb
📚 Learning: 2026-02-23T06:49:53.243Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: GEMINI.md:0-0
Timestamp: 2026-02-23T06:49:53.243Z
Learning: Applies to apps/mobile/lib/l10n/**/*.{arb,dart} : When updating `apps/mobile/lib/l10n/arb/*.arb`, regenerate or update `apps/mobile/lib/l10n/gen/*` so tracked outputs stay in sync.
Applied to files:
apps/mobile/lib/l10n/arb/app_vi.arb
📚 Learning: 2026-02-23T06:49:53.243Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: GEMINI.md:0-0
Timestamp: 2026-02-23T06:49:53.243Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Database `ws_id` columns ALWAYS store UUIDs. Route parameters (`wsId`) may contain special identifiers like `personal` or `internal`. NEVER use raw `wsId` directly in database queries. Use `normalizeWorkspaceId(wsId)` helper for API routes or accept `workspace` prop in components.
Applied to files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
📚 Learning: 2026-02-23T06:51:05.456Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T06:51:05.456Z
Learning: Applies to apps/*/src/app/api/**/*.ts : Resolve workspace ID parameters (`wsId`) to UUIDs before database operations; use `normalizeWorkspaceId()` for special identifiers like 'personal' and 'internal'
Applied to files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
📚 Learning: 2026-02-23T06:49:12.270Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-23T06:49:12.270Z
Learning: Applies to apps/**/src/components/**/*.{ts,tsx} : Use `workspace.id` directly from workspace object in client components instead of raw `wsId` parameter
Applied to files:
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts
📚 Learning: 2026-02-23T06:49:12.271Z
Learnt from: CR
Repo: tutur3u/platform PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-23T06:49:12.271Z
Learning: Applies to apps/mobile/lib/**/*.dart : Use `Future<void> Function()` callbacks for mutation-driven UI actions in Flutter; await before closing dialogs
Applied to files:
apps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dart
🧬 Code graph analysis (4)
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.ts (1)
packages/sdk/src/storage.ts (1)
request(807-927)
packages/hooks/src/hooks/use-missed-entry-form.ts (1)
packages/hooks/src/upload-time-tracking-images.ts (1)
uploadTimeTrackingImages(11-75)
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.ts (3)
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts (1)
POST(49-189)apps/web/src/lib/api-auth.ts (1)
withSessionAuth(174-300)packages/supabase/src/next/server.ts (1)
createDynamicAdminClient(187-198)
apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts (3)
packages/utils/src/constants.ts (1)
MAX_NAME_LENGTH(48-48)packages/sdk/src/storage.ts (1)
request(807-927)packages/supabase/src/next/server.ts (1)
createDynamicClient(132-167)
🔇 Additional comments (19)
apps/mobile/lib/l10n/arb/app_en.arb (1)
262-265: LGTM — new localization keys are well-formed and consistent.All four keys follow the existing
timerXxxnaming convention, have natural English copy, and require no placeholder annotations.apps/mobile/lib/l10n/arb/app_vi.arb (2)
262-265: Vietnamese translations look accurate and natural — LGTM.
262-265: The generated Dart localization files already contain all the new keys from the ARB update. No regeneration is required.Likely an incorrect or invalid review comment.
packages/hooks/src/upload-time-tracking-images.ts (1)
11-74: Signed-URL upload helper looks solid.
Clear validation and per-upload error handling make this utility reliable to reuse.apps/web/src/app/[locale]/(dashboard)/[wsId]/time-tracker/requests/hooks/use-request-mutations.ts (1)
294-311: Signed-URL paths are correctly wired into the update payload.apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/upload-url/route.ts (1)
44-147: Signed URL generation flow looks solid.apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/route.ts (2)
14-47: Schema + image path validation additions look good.
82-163: JSON-only input enforcement and signed-path handling are correctly applied.apps/web/src/app/api/v1/workspaces/[wsId]/time-tracking/requests/[id]/route.ts (1)
33-61: Edit payload schema and path validation are well-structured.apps/mobile/lib/data/repositories/time_tracker_repository.dart (2)
193-328: Signed-URL upload helper is well-validated.
Response shape checks and per-upload error handling look solid.
627-780: Create/update payloads correctly use uploaded image paths.apps/mobile/lib/features/time_tracker/cubit/time_tracker_cubit.dart (1)
566-586: Rename toimageLocalPathsis correctly propagated.apps/mobile/lib/features/time_tracker/view/time_tracker_requests_page.dart (1)
399-416: Rename propagation looks consistent.The onEdit wiring correctly forwards
newImageLocalPathsto the cubit update call.apps/mobile/lib/features/time_tracker/widgets/missed_entry_dialog.dart (1)
27-37: onSave signature rename aligns with local-path flow.apps/mobile/lib/features/time_tracker/widgets/timer_tab.dart (1)
299-343: Rename propagation looks consistent.The MissedEntryDialog onSave wiring correctly forwards
imageLocalPaths.apps/mobile/lib/features/time_tracker/widgets/edit_request_dialog.dart (2)
19-27: Signature rename looks good.
157-167: Call-site updated correctly.apps/mobile/lib/features/time_tracker/widgets/request_detail_sheet.dart (2)
41-49: Signature rename looks good.
332-353: Forwarding newImageLocalPaths is consistent.
- Added validation for start and end times in edit actions. - Refactored image upload logic to streamline request submissions. - Updated test teardown to ensure temporary directories are cleaned up. - Introduced new test cases for editing time tracking sessions. - Adjusted maximum file upload limits for time tracking requests. - Improved error handling for image removal and cleanup processes.
|
ready for merge @vhpx , no further changes will be made bar requested ones |
Description
Switched time tracking proof image uploads to signed URLs and moved request APIs to JSON for safer, faster uploads. Also updated missed-entry UX, added tests, and kept the mobile history view.
New Features
Migration