Security fix (Gemini key) + productivity features (notes, DnD, Pomodoro, palette)#8
Conversation
- Add deadline input to add/edit-project forms and persist `deadline`, fixing Schedule "Project Deadline" events that never rendered - Show project deadline as a badge on the project header - Rewrite handleDeleteProject to use writeBatch (atomic, faster) - Make DailyReminder data-driven (overdue/today counts) and render it on the Dashboard - Add aria-labels to icon-only buttons across TaskItem, Sidebar, HabitTrackerView, ProjectDetail, and the mobile menu toggle - Extract shared habit-card logic into useHabit hook, removing ~90 lines of duplication Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the Gemini API key from the client bundle. The web app now POSTs to a Cloudflare Worker (REACT_APP_GEMINI_PROXY_URL) with the user's Firebase ID token; the Worker verifies the token and forwards to Gemini using the key held as a Worker secret. Cloudflare's free tier keeps the Firebase project on the free Spark plan (Cloud Functions would require Blaze). - gemini-proxy/: Worker (Firebase ID-token verification, CORS lock), wrangler config, package.json, README with deploy steps - geminiService.js: route through proxy w/ ID token; direct-key path kept only as a local-dev fallback - env.js: add GEMINI_PROXY_URL + isGeminiConfigured - Dashboard, ProjectDetail, GoalPlannerModal: use isGeminiConfigured and the keyless call signature - README: document proxy env + production warning Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ALLOWED_ORIGIN now accepts a comma-separated list so AI calls work through the proxy in both production and local dev, letting the client drop the bundled dev key entirely. Also commits the generated package-lock.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 0 (AI robustness): - Worker returns a clear 500 when the GEMINI_API_KEY secret is missing instead of forwarding a keyless request - Client maps only proxy 401s to 'sign in again'; surfaces real upstream error messages for other statuses Phase 1: - Add per-project Notes & Resources (ProjectNotes.jsx): free-form notes with save-on-blur + a validated links list, stored on the project doc - Add Tasks/Notes tab switcher in ProjectDetail Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a 'Manual (drag to reorder)' sort option in ProjectDetail. Incomplete tasks become draggable via @dnd-kit with a grip handle; the new order is persisted to each task's `order` field (batched write) so it survives reloads and syncs across devices. Completed tasks render below, non-draggable. - SortableTaskList.jsx: DnD context, optimistic reorder with rollback - ProjectDetail: manual-order memo + conditional render - deps: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a Select mode to AllTasksView: per-row checkboxes, select-all over the current filter, and a bulk action bar to mark complete, reopen, or delete the selected tasks via batched Firestore writes (delete behind a confirm modal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an app-wide Pomodoro widget (PomodoroTimer.jsx) mounted in HubApp: Focus/Short/Long modes, circular progress dial, drift-resistant countdown using an absolute end time, a Web Audio chime on completion, auto-cycling (long break after every 4th focus block), and a localStorage-backed daily focus-session counter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Global command palette (CommandPalette.jsx) opened with Ctrl/Cmd+K or a header search button on mobile. Fuzzy-search projects and tasks with arrow-key navigation, jump to a project (or a task's project), or quick-add a task to a chosen project inline. Wired into HubApp with projects/tasks/navigation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Visit the preview URL for this PR (updated for commit e5ce655): https://my-productivity-hub-5a3ba--pr8-fix-audit-recommenda-fz789pzs.web.app (expires Sun, 07 Jun 2026 18:42:05 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 4ef75aef15076a8cc05555b91f31935d9a13db8e |
There was a problem hiding this comment.
Code Review
This pull request introduces a Cloudflare Worker proxy to securely handle Gemini API calls, keeping the API key off the client. It also adds several new features, including a Pomodoro Timer, a Ctrl/Cmd+K Command Palette, drag-and-drop task reordering via @dnd-kit, project notes/resources, and bulk task actions. Refactoring was done to share habit-card logic through a custom hook. Feedback on these changes includes adding configuration checks for FIREBASE_PROJECT_ID in the worker, defaulting entries to an empty array in useHabit to prevent crashes, wrapping localStorage and Firestore addDoc operations in try-catch blocks for robustness, and avoiding invalid HTML nesting of interactive elements inside <label> tags in AllTasksView.
| try { | ||
| await verifyFirebaseToken(token, env.FIREBASE_PROJECT_ID); | ||
| } catch (e) { | ||
| return json({ error: { message: `Invalid token: ${e.message}` } }, 401, origin); | ||
| } |
There was a problem hiding this comment.
The worker verifies the Firebase ID token against env.FIREBASE_PROJECT_ID, but there is no check to ensure this environment variable is actually configured. If it is missing, the verification will fail with a generic or confusing error. Adding a configuration check (similar to GEMINI_API_KEY) will improve debuggability.
if (!env.FIREBASE_PROJECT_ID) {
return json(
{ error: { message: 'Server misconfigured: missing FIREBASE_PROJECT_ID.' } },
500,
origin
);
}
try {
await verifyFirebaseToken(token, env.FIREBASE_PROJECT_ID);
} catch (e) {
return json({ error: { message: `Invalid token: ${e.message}` } }, 401, origin);
}| * mobile row and the tall desktop card. Keeps the two presentational components | ||
| * free of duplicated Firestore handlers. | ||
| */ | ||
| export default function useHabit(habit, entries) { |
There was a problem hiding this comment.
If entries is undefined or null (e.g., during initial load or if not passed by the parent component), calling entries.find will throw a TypeError. Defaulting entries to an empty array in the function signature prevents potential crashes.
| export default function useHabit(habit, entries) { | |
| export default function useHabit(habit, entries = []) { |
| const writeFocusCount = (count) => { | ||
| localStorage.setItem('prodhub_pomodoro_focus', JSON.stringify({ date: todayKey(), count })); | ||
| }; |
There was a problem hiding this comment.
In environments where localStorage is disabled or full, calling localStorage.setItem can throw a SecurityError or QuotaExceededError. Wrapping this call in a try-catch block ensures the application does not crash when a Pomodoro session completes.
const writeFocusCount = (count) => {
try {
localStorage.setItem('prodhub_pomodoro_focus', JSON.stringify({ date: todayKey(), count }));
} catch (e) {
console.warn('Failed to save Pomodoro focus count to localStorage:', e);
}
};| selectMode ? ( | ||
| <label | ||
| key={task.id} | ||
| className="flex items-center gap-2.5 cursor-pointer rounded-2xl" | ||
| > | ||
| <input | ||
| type="checkbox" | ||
| checked={selectedIds.has(task.id)} | ||
| onChange={() => toggleSelect(task.id)} | ||
| className="w-4 h-4 flex-shrink-0 rounded accent-blue-500" | ||
| /> | ||
| <div className="flex-grow min-w-0 pointer-events-none"> | ||
| <TaskItem task={task} projects={projects} /> | ||
| </div> | ||
| </label> | ||
| ) : ( |
There was a problem hiding this comment.
Nesting interactive elements (like the buttons inside TaskItem) inside a <label> is invalid HTML according to the HTML specification and can cause unexpected behavior with screen readers and keyboard navigation. Using a div with an onClick handler is semantically valid and safer.
selectMode ? (
<div
key={task.id}
onClick={() => toggleSelect(task.id)}
className="flex items-center gap-2.5 cursor-pointer rounded-2xl"
>
<input
type="checkbox"
checked={selectedIds.has(task.id)}
readOnly
className="w-4 h-4 flex-shrink-0 rounded accent-blue-500"
/>
<div className="flex-grow min-w-0 pointer-events-none">
<TaskItem task={task} projects={projects} />
</div>
</div>
) : (| } else if (item.type === 'add') { | ||
| const targetProject = addProjectId || projects[0]?.id; | ||
| if (!userId || !db || !targetProject || !item.label) return; | ||
| await addDoc(collection(db, `artifacts/${appId}/users/${userId}/tasks`), { | ||
| title: item.label, | ||
| priority: 'Medium', | ||
| projectId: targetProject, | ||
| completed: false, | ||
| createdAt: new Date(), | ||
| dueDate: '', | ||
| }); | ||
| onNavigate('project', targetProject); | ||
| onClose(); | ||
| } |
There was a problem hiding this comment.
The asynchronous addDoc call is not wrapped in a try-catch block. If the Firestore write fails (e.g., due to permission issues or network offline), it will result in an unhandled promise rejection. Wrapping it in a try-catch block improves robustness.
} else if (item.type === 'add') {
const targetProject = addProjectId || projects[0]?.id;
if (!userId || !db || !targetProject || !item.label) return;
try {
await addDoc(collection(db, `artifacts/${appId}/users/${userId}/tasks`), {
title: item.label,
priority: 'Medium',
projectId: targetProject,
completed: false,
createdAt: new Date(),
dueDate: '',
});
onNavigate('project', targetProject);
onClose();
} catch (error) {
console.error('Failed to add task:', error);
}
}Tasks gain a recurrence field. Completing a recurring task spawns its next occurrence exactly once (guarded by recurrenceSpawned), with the due date advanced by the interval via a new nextDueDate() util. Recurrence controls added to the add-task form (ProjectDetail) and the task edit form (TaskItem, both mobile and desktop layouts), plus a repeat indicator in full and compact task rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Synced calendar events are now saved to Firestore (meta/calendar doc) and rehydrated on load, so they survive page refreshes instead of living only in React state — no re-consent needed each session. Adds calendarStore.js (save + subscribe with Timestamp<->Date conversion), an App-level hydration listener, and a 'last synced' timestamp on the Schedule page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add src/utils/datetime.test.js (15 tests) covering getLocalDateKey, formatDate/formatTime, and the recurrence nextDueDate logic; add a 'test:ci' script (react-scripts test --watchAll=false) - Cap all App.js per-user subscriptions with limit() (READ_LIMITS) so reads are bounded instead of subscribing to whole collections unbounded Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Audit-driven batch: one security fix (Gemini API key no longer shipped to the browser) plus a set of UX/feature improvements, several from the project's own
waiting_Updates.mdroadmap. Every commit builds clean (npm run build).🔒 Security
gemini-proxy/) that verifies the user's Firebase ID token and forwards to Gemini with the key held as a Worker secret. CORS locked to app origin (prod + localhost). Verified the key is absent from the production bundle.geminiService.jsattaches the ID token; direct-key path kept only as a local-dev fallback. Clear error when the secret is missing.✨ Features
@dnd-kit, order persisted to aorderfield. (roadmap)Ctrl/Cmd+Kto search projects/tasks and quick-add a task; mobile search button.🛠 Quality
handleDeleteProjectnow uses an atomicwriteBatchinstead of a serial await loop.useHabithook (~90 lines of duplication removed).aria-labels to icon-only buttons across the app.Follow-up (not in this PR)
npm run build+firebase deployto push to production.wrangler secret put.🤖 Generated with Claude Code