Skip to content

Security fix (Gemini key) + productivity features (notes, DnD, Pomodoro, palette)#8

Merged
Rupesh4604 merged 11 commits into
mainfrom
fix/audit-recommendations
May 31, 2026
Merged

Security fix (Gemini key) + productivity features (notes, DnD, Pomodoro, palette)#8
Rupesh4604 merged 11 commits into
mainfrom
fix/audit-recommendations

Conversation

@Rupesh4604

Copy link
Copy Markdown
Owner

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.md roadmap. Every commit builds clean (npm run build).

🔒 Security

  • Gemini key moved server-side. The key previously shipped in the client bundle. AI calls now route through a free Cloudflare Worker (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.
    • Keeps Firebase on the free Spark plan (Cloud Functions would require Blaze).
    • Client: geminiService.js attaches the ID token; direct-key path kept only as a local-dev fallback. Clear error when the secret is missing.

✨ Features

  • Project deadlines — fixes a dead feature: deadlines never rendered on the Schedule because no UI ever set them. Added date inputs to add/edit-project forms + a header badge.
  • Project Notes & Resources — per-project Notes tab + validated links list. (roadmap)
  • Drag-and-drop task reordering — "Manual" sort mode via @dnd-kit, order persisted to a order field. (roadmap)
  • Bulk task actions — select mode in All Tasks: complete / reopen / delete multiple (batched writes).
  • Pomodoro timer — floating widget on every view: Focus/Short/Long modes, dial, chime, daily session counter. (roadmap)
  • Command paletteCtrl/Cmd+K to search projects/tasks and quick-add a task; mobile search button.
  • Daily Reminder — reworked from a static, unrendered banner into a data-driven Dashboard card.

🛠 Quality

  • handleDeleteProject now uses an atomic writeBatch instead of a serial await loop.
  • Extracted shared habit-card logic into a useHabit hook (~90 lines of duplication removed).
  • Added aria-labels to icon-only buttons across the app.

Follow-up (not in this PR)

  • Deploy: npm run build + firebase deploy to push to production.
  • Rotate the old Gemini key (it was public); set the new one via wrangler secret put.
  • Remaining roadmap: recurring tasks, persist calendar sync, notifications, light theme, tests.

🤖 Generated with Claude Code

Rupesh4604 and others added 8 commits May 31, 2026 23:02
- 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>
@github-actions

github-actions Bot commented May 31, 2026

Copy link
Copy Markdown

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread gemini-proxy/src/index.js
Comment on lines +106 to +110
try {
await verifyFirebaseToken(token, env.FIREBASE_PROJECT_ID);
} catch (e) {
return json({ error: { message: `Invalid token: ${e.message}` } }, 401, origin);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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.

Suggested change
export default function useHabit(habit, entries) {
export default function useHabit(habit, entries = []) {

Comment on lines +15 to +17
const writeFocusCount = (count) => {
localStorage.setItem('prodhub_pomodoro_focus', JSON.stringify({ date: todayKey(), count }));
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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);
  }
};

Comment on lines +186 to +201
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>
) : (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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>
            ) : (

Comment on lines +75 to +88
} 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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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);
      }
    }

Rupesh4604 and others added 3 commits May 31, 2026 23:57
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>
@Rupesh4604 Rupesh4604 merged commit 3c2e500 into main May 31, 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.

1 participant