Skip to content

Add task preset feature with security review docs and UX improvements#2

Draft
manuelgruber wants to merge 6 commits intomainfrom
C/security-review
Draft

Add task preset feature with security review docs and UX improvements#2
manuelgruber wants to merge 6 commits intomainfrom
C/security-review

Conversation

@manuelgruber
Copy link
Copy Markdown
Member

@manuelgruber manuelgruber commented Apr 25, 2026

Summary

  • Adds a TaskPreset database model, service, and built-in seed data to support predefined reserve configurations
  • Adds REST API endpoints and a React UI for selecting and applying task presets in the Planner page
  • Improves responsive layout in the Header and Planner, and refines Tooltip component styling
  • Upgrades frontend dependencies to latest versions (package.json / package-lock.json refresh)
  • Adds security remediation plan and UX improvement roadmap to docs/plans

Test plan

  • Start the app and verify the Planner page shows preset options
  • Apply a preset and confirm the reserve value updates correctly
  • Verify /api/v1/tasks/presets returns seeded preset data
  • Check responsive layout on mobile and desktop screen sizes
  • Run pytest tests/ to confirm no backend regressions
  • Run npm run test and npm run type-check for frontend checks

Summary by CodeRabbit

  • New Features

    • Task preset management: save, load, and delete reusable task configurations
    • Mobile navigation dropdown menu
    • Touch-friendly tooltip interactions
    • Enhanced mobile responsiveness across dashboard, planner, and settings pages
  • Documentation

    • Added security remediation roadmap
    • Added UX improvement roadmap
  • Chores

    • Updated frontend and backend dependencies

Bumps React to 19, react-router-dom to v7, date-fns to v4, and all
associated type definitions and dev tooling to current releases.
Introduces the TaskPreset table for storing reusable task templates.
Adds TaskPresetService with CRUD operations and a preset_exists_by_name
guard used during seeding. run_migration now seeds seven built-in presets
(reserve levels, grid charging, operating modes, grid export) on startup
if they are not already present.
Adds GET /tasks/presets, POST /tasks/presets, and DELETE
/tasks/presets/<id> under the tasks blueprint. Built-in presets are
protected from deletion (403). Input is validated against CommandType
and CronCommand before persisting.
Planner page gains a preset selector (grouped by built-in vs user),
"Save as Preset" modal, and delete button for user-defined presets.
TaskPreset type and three api client methods (getTaskPresets,
createTaskPreset, deleteTaskPreset) are added.

Responsive fixes across the app: mobile hamburger nav in Header,
horizontal scroll on LogsTable, touch-dismiss support in Tooltip,
flex-wrap on Dashboard status bar and Settings form rows, and
stacked action buttons in the Planner task table on small screens.
Copilot AI review requested due to automatic review settings April 25, 2026 00:42
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Walkthrough

This pull request introduces task preset management to PowerNight: a new database model, service layer, and API endpoints enable users to create, list, and delete reusable task templates. Frontend updates add responsive design improvements to multiple components and pages, along with dependency upgrades. Documentation plans for security hardening and UX improvements are also included.

Changes

Cohort / File(s) Summary
Documentation Plans
docs/plans/2026-04-25-security-remediation.md, docs/plans/ux-improvement-roadmap.md
New planning documents outlining multi-phase security remediation steps (authentication, encryption, headers, error handling) and UX roadmap (quick wins, dashboards, task management, dark mode, onboarding).
Frontend Dependencies
package.json
Runtime dependencies (axios, date-fns, react, react-router-dom) and dev dependencies (Playwright, Tailwind, Testing Library, TypeScript ESLint, Vite) bumped to newer versions via version range updates.
Task Preset Database Layer
src/powernight/core/database/exceptions.py, src/powernight/core/database/models.py, src/powernight/core/database/services.py, src/powernight/core/database/__init__.py
New PresetNotFoundError exception, TaskPreset model with serialization, and TaskPresetService for CRUD operations (create, list, get, delete with builtin protection, name existence checks).
Task Preset Migration & Seeding
src/powernight/core/database/migration.py
Adds builtin preset dataset and seed_builtin_presets() routine to initialize database with predefined presets on first run.
Task Preset API Endpoints
src/powernight/web/api/tasks_api.py
Three authenticated routes for preset management: GET /api/v1/tasks/presets (list), POST /api/v1/tasks/presets (create with validation), DELETE /api/v1/tasks/presets/<preset_id> (delete with error handling).
Responsive Layout Improvements
src/powernight/web/src/components/Header.tsx, src/powernight/web/src/components/LogsTable.tsx, src/powernight/web/src/pages/Dashboard.tsx, src/powernight/web/src/pages/Settings.tsx
Mobile navigation dropdown for Header, horizontal scrolling for LogsTable, wrapping flex layout for Dashboard section header, and full-width buttons on small screens in Settings.
Touch & Interaction Enhancements
src/powernight/web/src/components/Tooltip.tsx
Added touch event listeners (touchstart, mousedown) for dismissing tooltips on mobile and preventing default touch behavior.
Planner Page with Preset Integration
src/powernight/web/src/pages/Planner.tsx
Loads and displays task presets on mount; adds preset selector, form population from selected preset, "Save as Preset" modal with creation/deletion, and responsive layout adjustments (hide "Last Execution" on small screens).
Frontend Types & API Layer
src/powernight/web/src/types/index.ts, src/powernight/web/src/utils/api.ts
New TaskPreset TypeScript interface and three API client methods (getTaskPresets, createTaskPreset, deleteTaskPreset) with error handling.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Planner as Planner Page
    participant API as API Routes
    participant Service as TaskPresetService
    participant DB as Database

    User->>Planner: Load page / Select preset / Save as preset / Delete preset
    Planner->>API: GET /api/v1/tasks/presets
    API->>Service: list_presets()
    Service->>DB: Query TaskPreset records
    DB-->>Service: Return presets (ordered by sort_order, name)
    Service-->>API: Return presets array + total
    API-->>Planner: Return { success, presets, total }

    User->>Planner: Fill form & click "Save as Preset"
    Planner->>API: POST /api/v1/tasks/presets (name, command, command_params, default_time)
    API->>API: Validate command, command_params, default_time
    API->>Service: create_preset(...)
    Service->>DB: Insert TaskPreset with is_builtin=false
    DB-->>Service: Return created preset
    Service-->>API: Return created preset dict
    API-->>Planner: Return { success, preset }
    Planner->>Planner: Refresh preset list, clear form

    User->>Planner: Select preset to delete
    Planner->>API: DELETE /api/v1/tasks/presets/<preset_id>
    API->>Service: delete_preset(preset_id)
    Service->>DB: Check is_builtin flag
    alt is_builtin == true
        Service-->>API: Raise DatabaseError (blocked)
        API-->>Planner: Return { success: false, error: "...", status: 403 }
    else is_builtin == false
        DB->>DB: Delete preset record
        Service-->>API: Return true
        API-->>Planner: Return { success: true, status: 200 }
    end
    Planner->>Planner: Refresh presets, clear selection if needed
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A preset per task, now bundled with care,
Templates for power, saved everywhere!
From seeds in the database to forms on the page,
Touch-friendly scrolls for the mobile age,
Security plans and UX dreams take flight—
The burrow's getting cozier, and oh, so right! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: addition of task preset functionality, security review documentation, and UX improvements across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 94.44% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch C/security-review

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/powernight/core/database/services.py (1)

779-780: Consider preserving exception chain for debugging.

The exception re-raising pattern loses the original traceback. Using raise ... from e would help with debugging.

♻️ Optional improvement
         except Exception as e:
-            raise DatabaseError(f"Failed to create preset: {e}")
+            raise DatabaseError(f"Failed to create preset: {e}") from e

Apply the same pattern to the other except Exception as e blocks in this service.

Also applies to: 790-791, 805-806, 824-825

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/powernight/core/database/services.py` around lines 779 - 780, The
DatabaseError raised in the except blocks currently discards the original
traceback; update the re-raises to preserve the exception chain by using "raise
DatabaseError(f'Failed to create preset: {e}') from e" (replace the message as
appropriate) in the except block shown and apply the same pattern to the other
handlers in this service (the other except Exception as e blocks that raise
DatabaseError around the create/preset logic) so the original exception is
chained to DatabaseError for proper debugging.
src/powernight/web/api/tasks_api.py (1)

802-809: Fragile string matching to detect builtin preset deletion.

The check if "Built-in" in str(e) couples the API layer to the exact error message text from TaskPresetService.delete_preset(). If the message changes, this will return 500 instead of 403.

Consider introducing a specific exception type (e.g., BuiltinPresetError) for this case.

♻️ Suggested approach

In exceptions.py:

class BuiltinPresetError(DatabaseError):
    """Raised when attempting to modify a built-in preset."""
    pass

In services.py:

 if preset.is_builtin:
-    raise DatabaseError("Built-in presets cannot be deleted")
+    raise BuiltinPresetError("Built-in presets cannot be deleted")

In tasks_api.py:

+    except BuiltinPresetError as e:
+        return jsonify({
+            'success': False,
+            'error': 'Forbidden',
+            'message': str(e),
+            'timestamp': datetime.now(timezone.utc).isoformat()
+        }), 403
+
     except DatabaseError as e:
-        if "Built-in" in str(e):
-            return jsonify({
-                'success': False,
-                'error': 'Forbidden',
-                'message': str(e),
-                'timestamp': datetime.now(timezone.utc).isoformat()
-            }), 403
         return jsonify({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/powernight/web/api/tasks_api.py` around lines 802 - 809, Replace the
fragile string-match by introducing a dedicated exception and using it
end-to-end: add BuiltinPresetError(DatabaseError) in exceptions.py, update
TaskPresetService.delete_preset (or the method that raises DatabaseError on
built-in deletion) to raise BuiltinPresetError instead of raising DatabaseError
with the "Built-in" text, then in tasks_api.py import BuiltinPresetError and
change the handler to except BuiltinPresetError as e: return the 403 JSON
response (and keep a general except DatabaseError fallback for other DB errors);
ensure imports are updated accordingly.
src/powernight/web/src/pages/Planner.tsx (1)

69-76: Consider surfacing preset loading errors to users.

Unlike handleSavePreset and handleDeletePreset which set the error state, loadPresets silently logs errors to the console. If presets fail to load, users won't see any indication.

♻️ Optional: Add error feedback
   const loadPresets = async () => {
     try {
       const data = await api.getTaskPresets();
       setPresets(data.presets || []);
     } catch (err) {
       console.error('Failed to load presets:', err);
+      // Optionally notify user, though this is non-critical
+      // setError('Failed to load presets');
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/powernight/web/src/pages/Planner.tsx` around lines 69 - 76, loadPresets
currently swallows errors by only logging to console; update its error handling
to mirror handleSavePreset/handleDeletePreset by calling the component error
setter (e.g., setError) inside the catch with a user-facing message (and
optionally the err.message), and clear that error on successful load (e.g., call
setError(null/undefined) before or after setPresets). Specifically modify the
loadPresets function that calls api.getTaskPresets to setPresets(data.presets ||
[]) on success and setError(...) on failure so users receive visible feedback
when presets fail to load.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/powernight/web/src/components/Header.tsx`:
- Around line 55-60: The mobile nav toggle removed the browser focus ring making
keyboard use impossible; in the Header component update the toggle button
(onClick={() => setMenuOpen(!menuOpen)}, aria-expanded={menuOpen}) and the
mobile menu items to restore a visible keyboard focus state instead of using
focus:outline-none — either remove that utility or replace it with an accessible
focus style (e.g. focus-visible:ring / focus-visible:outline or a clear
background/border change) and apply the same visible focus treatment to the
corresponding menu item elements rendered in the collapse (also present around
the elements referenced near the other block at lines showing the same pattern).
Ensure the aria-expanded logic and setMenuOpen remain unchanged while only
altering classes/styles for keyboard focus visibility.

In `@src/powernight/web/src/components/Tooltip.tsx`:
- Around line 112-124: The touch handler handleTouchStart currently calls
e.preventDefault(), which blocks follow-up clicks on touch devices and breaks
interactive children; remove the e.preventDefault() call (or conditionally skip
it for interactive elements) and simply toggle setIsVisible((prev) => !prev) so
the tooltip still opens on touch without cancelling the native click for
links/buttons wrapped by triggerRef.

In `@src/powernight/web/src/utils/api.ts`:
- Around line 345-356: The createTaskPreset method references the CommandType
type but it isn't imported, causing a TypeScript error; add an import for
CommandType from the module that exports your types (e.g., import { CommandType
} from '../types') at the top of the file so the type annotation on
createTaskPreset is resolved; ensure the import is included alongside other
imports in src/powernight/web/src/utils/api.ts and that the symbol name exactly
matches CommandType used in the function signature.

---

Nitpick comments:
In `@src/powernight/core/database/services.py`:
- Around line 779-780: The DatabaseError raised in the except blocks currently
discards the original traceback; update the re-raises to preserve the exception
chain by using "raise DatabaseError(f'Failed to create preset: {e}') from e"
(replace the message as appropriate) in the except block shown and apply the
same pattern to the other handlers in this service (the other except Exception
as e blocks that raise DatabaseError around the create/preset logic) so the
original exception is chained to DatabaseError for proper debugging.

In `@src/powernight/web/api/tasks_api.py`:
- Around line 802-809: Replace the fragile string-match by introducing a
dedicated exception and using it end-to-end: add
BuiltinPresetError(DatabaseError) in exceptions.py, update
TaskPresetService.delete_preset (or the method that raises DatabaseError on
built-in deletion) to raise BuiltinPresetError instead of raising DatabaseError
with the "Built-in" text, then in tasks_api.py import BuiltinPresetError and
change the handler to except BuiltinPresetError as e: return the 403 JSON
response (and keep a general except DatabaseError fallback for other DB errors);
ensure imports are updated accordingly.

In `@src/powernight/web/src/pages/Planner.tsx`:
- Around line 69-76: loadPresets currently swallows errors by only logging to
console; update its error handling to mirror handleSavePreset/handleDeletePreset
by calling the component error setter (e.g., setError) inside the catch with a
user-facing message (and optionally the err.message), and clear that error on
successful load (e.g., call setError(null/undefined) before or after
setPresets). Specifically modify the loadPresets function that calls
api.getTaskPresets to setPresets(data.presets || []) on success and
setError(...) on failure so users receive visible feedback when presets fail to
load.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9dc854b8-866c-4b0e-a5cc-09913467a6e8

📥 Commits

Reviewing files that changed from the base of the PR and between d12f2a5 and 5d11973.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • docs/plans/2026-04-25-security-remediation.md
  • docs/plans/ux-improvement-roadmap.md
  • package.json
  • src/powernight/core/database/__init__.py
  • src/powernight/core/database/exceptions.py
  • src/powernight/core/database/migration.py
  • src/powernight/core/database/models.py
  • src/powernight/core/database/services.py
  • src/powernight/web/api/tasks_api.py
  • src/powernight/web/src/components/Header.tsx
  • src/powernight/web/src/components/LogsTable.tsx
  • src/powernight/web/src/components/Tooltip.tsx
  • src/powernight/web/src/pages/Dashboard.tsx
  • src/powernight/web/src/pages/Planner.tsx
  • src/powernight/web/src/pages/Settings.tsx
  • src/powernight/web/src/types/index.ts
  • src/powernight/web/src/utils/api.ts

Comment on lines +55 to +60
<button
onClick={() => setMenuOpen(!menuOpen)}
className="text-white p-2 rounded-md hover:bg-white/10 focus:outline-none"
aria-label="Toggle navigation menu"
aria-expanded={menuOpen}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore visible keyboard focus for the mobile navigation.

The new small-screen nav removes the toggle’s default focus outline and doesn’t add a replacement focus state for the menu items. That makes the collapsed navigation hard to use with a keyboard.

Suggested fix
-              className="text-white p-2 rounded-md hover:bg-white/10 focus:outline-none"
+              className="text-white p-2 rounded-md hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-blue-600"
...
-                className={`block px-4 py-2 text-sm font-bold transition-colors ${
+                className={`block px-4 py-2 text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/80 ${

Also applies to: 82-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/powernight/web/src/components/Header.tsx` around lines 55 - 60, The
mobile nav toggle removed the browser focus ring making keyboard use impossible;
in the Header component update the toggle button (onClick={() =>
setMenuOpen(!menuOpen)}, aria-expanded={menuOpen}) and the mobile menu items to
restore a visible keyboard focus state instead of using focus:outline-none —
either remove that utility or replace it with an accessible focus style (e.g.
focus-visible:ring / focus-visible:outline or a clear background/border change)
and apply the same visible focus treatment to the corresponding menu item
elements rendered in the collapse (also present around the elements referenced
near the other block at lines showing the same pattern). Ensure the
aria-expanded logic and setMenuOpen remain unchanged while only altering
classes/styles for keyboard focus visibility.

Comment on lines +112 to 124
const handleTouchStart = (e: React.TouchEvent) => {
e.preventDefault();
setIsVisible((prev) => !prev);
};

return (
<>
<div
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
className="inline-block"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t cancel all touch defaults on the trigger.

preventDefault() here blocks the follow-up click on touch devices, so wrapped interactive children like links or buttons can stop working. Toggle the tooltip without cancelling the touch event.

Suggested fix
-  const handleTouchStart = (e: React.TouchEvent) => {
-    e.preventDefault();
+  const handleTouchStart = () => {
     setIsVisible((prev) => !prev);
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleTouchStart = (e: React.TouchEvent) => {
e.preventDefault();
setIsVisible((prev) => !prev);
};
return (
<>
<div
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
className="inline-block"
const handleTouchStart = () => {
setIsVisible((prev) => !prev);
};
return (
<>
<div
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
className="inline-block"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/powernight/web/src/components/Tooltip.tsx` around lines 112 - 124, The
touch handler handleTouchStart currently calls e.preventDefault(), which blocks
follow-up clicks on touch devices and breaks interactive children; remove the
e.preventDefault() call (or conditionally skip it for interactive elements) and
simply toggle setIsVisible((prev) => !prev) so the tooltip still opens on touch
without cancelling the native click for links/buttons wrapped by triggerRef.

Comment on lines +345 to +356
async createTaskPreset(preset: {
name: string;
command: CommandType;
command_params: Record<string, any>;
default_time?: string;
}): Promise<TaskPreset> {
const response = await this.client.post<ApiResponse<TaskPreset>>('/tasks/presets', preset);
if (!response.data.success) {
throw new Error(response.data.error || 'Failed to create preset');
}
return response.data.data!;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing CommandType import causes TypeScript error.

The createTaskPreset method uses CommandType in its parameter type at line 347, but CommandType is not imported from '../types'. This will fail type-checking.

🐛 Proposed fix
 import {
   ApiResponse,
   SystemStatus,
   BackupReserveData,
   ScheduleEntry,
   HealthStatus,
   Metrics,
   ActivityEntry,
   NotificationConfig,
   ReserveFormData,
   ScheduleFormData,
   Task,
   TaskFormData,
   TaskExecution,
   TaskExecutionLogsResponse,
   CommandDefinition,
-  TaskPreset
+  TaskPreset,
+  CommandType
 } from '../types';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/powernight/web/src/utils/api.ts` around lines 345 - 356, The
createTaskPreset method references the CommandType type but it isn't imported,
causing a TypeScript error; add an import for CommandType from the module that
exports your types (e.g., import { CommandType } from '../types') at the top of
the file so the type annotation on createTaskPreset is resolved; ensure the
import is included alongside other imports in
src/powernight/web/src/utils/api.ts and that the symbol name exactly matches
CommandType used in the function signature.

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

Adds a “Task Preset” feature spanning database, backend API, and Planner UI to support reusable predefined task configurations, alongside several responsive UX refinements and planning docs updates.

Changes:

  • Introduces TaskPreset persistence (model/service) and seeds built-in presets during migration.
  • Adds REST endpoints for listing/creating/deleting presets and wires them into the React Planner UI.
  • Improves mobile/responsive UX across Header/Planner/Dashboard/LogsTable and updates Tooltip touch behavior; updates frontend dependencies.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/powernight/web/src/utils/api.ts Adds typed client methods for task preset endpoints.
src/powernight/web/src/types/index.ts Introduces TaskPreset TypeScript type.
src/powernight/web/src/pages/Settings.tsx Improves responsive form layout for inputs/buttons.
src/powernight/web/src/pages/Planner.tsx Adds preset loading/selection, “save as preset” modal, and responsive table tweaks.
src/powernight/web/src/pages/Dashboard.tsx Makes section header layout wrap better on smaller screens.
src/powernight/web/src/components/Tooltip.tsx Adds touch toggle + outside-tap dismissal behavior.
src/powernight/web/src/components/LogsTable.tsx Adds horizontal scrolling container + min column widths for mobile.
src/powernight/web/src/components/Header.tsx Adds mobile hamburger menu and hides desktop nav on small screens.
src/powernight/web/api/tasks_api.py Adds /tasks/presets GET/POST and /tasks/presets/<id> DELETE endpoints.
src/powernight/core/database/services.py Adds TaskPresetService for CRUD operations.
src/powernight/core/database/models.py Adds TaskPreset SQLAlchemy model + serialization.
src/powernight/core/database/migration.py Seeds built-in presets during migration run.
src/powernight/core/database/exceptions.py Adds PresetNotFoundError.
src/powernight/core/database/init.py Exports TaskPreset and PresetNotFoundError.
package.json Updates frontend dependencies/tooling versions.
docs/plans/ux-improvement-roadmap.md Adds UX roadmap planning document.
docs/plans/2026-04-25-security-remediation.md Adds security remediation planning document.

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

Comment on lines +438 to +450
{selectedPresetId && (() => {
const selected = presets.find(p => p.id === selectedPresetId);
return selected && !selected.is_builtin ? (
<button
type="button"
onClick={() => handleDeletePreset(selectedPresetId)}
className="px-3 py-2 text-red-600 border border-red-300 rounded-md hover:bg-red-50"
title="Delete this preset"
>
Delete
</button>
) : null;
})()}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

Using an IIFE inside JSX ({selectedPresetId && (() => { ... })()}) makes this render path harder to read/debug and is easy to accidentally break with hooks/state. Consider computing selectedPreset once (e.g., via a local const or useMemo) and rendering conditionally without an inline function call.

Suggested change
{selectedPresetId && (() => {
const selected = presets.find(p => p.id === selectedPresetId);
return selected && !selected.is_builtin ? (
<button
type="button"
onClick={() => handleDeletePreset(selectedPresetId)}
className="px-3 py-2 text-red-600 border border-red-300 rounded-md hover:bg-red-50"
title="Delete this preset"
>
Delete
</button>
) : null;
})()}
{selectedPresetId && presets.find(p => p.id === selectedPresetId)?.is_builtin === false && (
<button
type="button"
onClick={() => handleDeletePreset(selectedPresetId)}
className="px-3 py-2 text-red-600 border border-red-300 rounded-md hover:bg-red-50"
title="Delete this preset"
>
Delete
</button>
)}

Copilot uses AI. Check for mistakes.
Comment on lines +708 to +715
if not request.is_json:
raise ValidationError("Request must contain JSON data")

data = request.get_json()

# Validate required fields
if not data.get('name', '').strip():
raise ValidationError("Preset name is required")
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

request.get_json() can return None (or raise on invalid JSON) even when request.is_json is true. Consider using request.get_json(silent=True) and explicitly validating data is a dict before accessing .get(...) to avoid returning a 500 for malformed bodies.

Copilot uses AI. Check for mistakes.
Comment on lines +679 to +687
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
<h3 className="text-lg font-semibold mb-4">Save as Preset</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Preset Name
</label>
<input
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The new "Save as Preset" modal is implemented inline here, duplicating modal markup/behavior (overlay, close handling, keyboard behavior) and lacks basic dialog semantics (role="dialog", aria-modal, labelled-by). Consider reusing a shared modal component (or introducing one) to ensure consistent accessibility and interaction patterns.

Suggested change
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
<h3 className="text-lg font-semibold mb-4">Save as Preset</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Preset Name
</label>
<input
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowSavePresetModal(false);
}
}}
>
<div
className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full mx-4"
role="dialog"
aria-modal="true"
aria-labelledby="save-preset-modal-title"
onKeyDown={(e) => {
if (e.key === 'Escape') {
setShowSavePresetModal(false);
}
}}
>
<h3 id="save-preset-modal-title" className="text-lg font-semibold mb-4">Save as Preset</h3>
<div className="space-y-4">
<div>
<label htmlFor="preset-name-input" className="block text-sm font-medium text-gray-700 mb-1">
Preset Name
</label>
<input
id="preset-name-input"

Copilot uses AI. Check for mistakes.
Comment on lines +727 to +733
# Validate command parameters if provided
command_params = data.get('command_params', {})
if command_params:
command = CronCommand(data['command'], command_params)
validation = command.validate()
if not validation.valid:
raise ValidationError(f"Invalid command parameters: {', '.join(validation.errors)}")
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

command_params is assumed to be an object/dict, but a non-dict JSON value (string/array) will cause CronCommand(...).validate() to throw and be reported as a 500. Add a type check (e.g., ensure isinstance(command_params, dict)) and return a 422 Validation Error when it's not a mapping.

Copilot uses AI. Check for mistakes.
Comment on lines +693 to +700
except Exception as e:
current_app.logger.error(f"Failed to list presets: {e}")
return jsonify({
'success': False,
'error': 'Internal Server Error',
'message': f'Failed to list presets: {e}',
'timestamp': datetime.now(timezone.utc).isoformat()
}), 500
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

These error responses include the raw exception text ({e}) in the JSON payload. This can leak internal details to clients; log the exception server-side, but return a generic public message in the response body.

Copilot uses AI. Check for mistakes.
Comment on lines +770 to +777
except Exception as e:
current_app.logger.error(f"Failed to create preset: {e}")
return jsonify({
'success': False,
'error': 'Internal Server Error',
'message': f'Failed to create preset: {e}',
'timestamp': datetime.now(timezone.utc).isoformat()
}), 500
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The 500 handler returns message: f'Failed to create preset: {e}', exposing internal exception details to the client. Prefer returning a generic message (and optionally a request ID) while logging the full exception server-side.

Copilot uses AI. Check for mistakes.
Comment on lines +802 to +809
except DatabaseError as e:
if "Built-in" in str(e):
return jsonify({
'success': False,
'error': 'Forbidden',
'message': str(e),
'timestamp': datetime.now(timezone.utc).isoformat()
}), 403
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

Detecting the "built-in preset" case via substring matching on str(e) is brittle (message changes will break behavior). Prefer raising/catching a dedicated exception for forbidden built-in deletion (or an explicit error code) and mapping that to a 403 here.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +113
const handleTouchStart = (e: React.TouchEvent) => {
e.preventDefault();
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

Calling preventDefault() on touchstart can block native scrolling and other default touch interactions on mobile when the user starts a scroll gesture on a tooltip trigger. Consider removing preventDefault (or only preventing default for specific non-scroll cases) and using a click/pointer handler to toggle visibility.

Suggested change
const handleTouchStart = (e: React.TouchEvent) => {
e.preventDefault();
const handleTouchStart = () => {

Copilot uses AI. Check for mistakes.
<div className="flex items-center md:hidden">
<button
onClick={() => setMenuOpen(!menuOpen)}
className="text-white p-2 rounded-md hover:bg-white/10 focus:outline-none"
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The mobile menu button removes the default focus outline (focus:outline-none) but doesn't add a replacement focus style. Add a visible focus indicator (e.g., focus-visible:ring-*) so keyboard users can see focus location.

Suggested change
className="text-white p-2 rounded-md hover:bg-white/10 focus:outline-none"
className="text-white p-2 rounded-md hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-blue-600"

Copilot uses AI. Check for mistakes.
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.

2 participants