Skip to content

Slideshow management fixes: delete inactive items, duplicate slideshows - #23

Merged
jantman merged 19 commits into
mainfrom
slideshow-management-fixes
Jun 6, 2026
Merged

Slideshow management fixes: delete inactive items, duplicate slideshows#23
jantman merged 19 commits into
mainfrom
slideshow-management-fixes

Conversation

@jantman

@jantman jantman commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the slideshow-management-fixes feature (plan and progress in docs/features/completed/slideshow-management-fixes.md):

  1. Allow deletion of inactive slideshow items:
    DELETE /api/v1/slideshow-items/<id> previously returned 404 for inactive
    items, so an item that had been soft-deleted could never be removed even
    though the admin UI offers a Delete button on inactive items. Deletion is
    now two-stage: deleting an active item soft-deletes it (unchanged), and
    deleting an already-inactive item permanently removes it. The admin UI
    warns that deleting an inactive item is permanent.
  2. Duplicate slideshow: a new POST /api/v1/slideshows/<id>/duplicate
    endpoint and a Duplicate action on the Slideshows admin page that prompts
    for the copy's name (pre-filled "{name} (Copy)"). The duplicate copies all
    items (including inactive ones, which stay inactive), physically copies
    uploaded media files into the new slideshow's upload directory so the copy
    is fully independent, is owned by the current user, and is never the
    default slideshow.

Also includes a human-approved toolchain update: embrace black 26.5.x
formatting for Python 3.14 (PEP 758 except clauses) and target mypy at
Python 3.14 (mypy.ini), replacing the stale black!=26.1.0 exclusion.

Bumps version 0.3.5 → 0.4.0 and updates docs/api.rst / docs/usage.rst.

Test plan

  • Regression tests for inactive item deletion (unit + Playwright), written first and confirmed failing before the fix
  • Unit suite: 686 passed (12 new duplicate-endpoint tests incl. file copying, missing-file tolerance, name-conflict backstop, commit-failure file cleanup; 3 storage-helper tests)
  • Integration suite: 176 passed (duplicate happy path incl. inactive-item copying, cancelled prompt, duplicate-name error; inactive-item permanent delete)
  • Frontend suite: 137 passed; type-check, lint, and production build clean
  • nox -s format, lint, type_check, docs all passing

🤖 Generated with Claude Code

jantman and others added 7 commits June 6, 2026 06:31
…ent.

Documents the root-cause analysis and a three-milestone implementation plan
for the two slideshow management fixes:

- Milestone 1: Allow deletion of inactive slideshow items. The DELETE
  /api/v1/slideshow-items/<id> endpoint currently returns 404 for inactive
  items; the fix makes deletion two-stage (active items soft-delete as today,
  inactive items are hard-deleted), beginning with initially-failing
  regression tests.
- Milestone 2: Add a duplicate-slideshow capability via a new
  POST /api/v1/slideshows/<id>/duplicate endpoint and a Duplicate action on
  the Slideshows admin page, copying all items (including inactive) and
  physically copying uploaded media files into the new slideshow's upload
  directory.
- Milestone 3: Acceptance criteria (documentation, test coverage, full nox
  pass, minor version bump, PR).

Also records the human-approved design decisions: hard delete for inactive
items, prompt for the duplicate's name, copy media files on disk, and copy
all items including inactive ones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctive item deletion.

Adds regression tests that document the desired two-stage deletion behavior
for slideshow items:

- tests/unit/test_api.py:
  - test_delete_inactive_slideshow_item: deleting an inactive item should
    return 200 and permanently remove the row from the database.
  - test_delete_active_then_inactive_slideshow_item: first delete soft-deletes
    (is_active=False, row remains), second delete permanently removes.
- tests/integration/test_slideshow_items.py:
  - test_delete_inactive_item_permanently: soft-deletes an item via the API,
    then deletes it from the inactive state via the admin UI and verifies the
    row disappears from the item list.

All three tests currently fail because DELETE /api/v1/slideshow-items/<id>
returns 404 for inactive items ("if not item or not item.is_active" in
delete_slideshow_item). They will pass once the endpoint implements the
two-stage delete in task 1.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… items via two-stage delete.

Fixes the bug where inactive slideshow items could never be removed: the
DELETE /api/v1/slideshow-items/<id> endpoint returned 404 for any item with
is_active=False, while the admin UI displays inactive items with a Delete
button, so deleting them always failed.

The endpoint now implements a two-stage delete:

- Deleting an active item soft-deletes it (sets is_active=False), preserving
  the existing behavior so items can still be reactivated via the edit form.
- Deleting an already-inactive item permanently removes the row from the
  database via db.session.delete().

The item's title is captured before commit since a hard-deleted instance's
attributes are inaccessible after the commit; the SSE broadcast now uses the
item_id route parameter for the same reason. The endpoint docstring documents
the two-stage behavior.

The regression tests added in task 1.1 now pass; full unit suite passes
(671 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…is permanent.

Since deletion is now two-stage (active items are soft-deleted, inactive
items are permanently removed), the admin UI's delete confirmation should
distinguish the two cases. SlideshowDetail's handleDeleteItem now takes the
item's is_active flag and shows "Are you sure you want to permanently delete
... This cannot be undone." for inactive items, while keeping the original
confirmation message for active items.

Adds a frontend unit test covering the permanent-delete confirmation message
and DELETE call for an inactive item. Full frontend suite passes
(133 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…py at Python 3.14.

Side quest from the Slideshow Management Fixes feature: running nox -s format
with a freshly created environment installed black 26.5.1, which (with our
target-version of py314) rewrites "except (X, Y):" to the PEP 758 style
"except X, Y:" and hugs multiline string arguments. The previous safeguard of
excluding black 26.1.0 in noxfile.py no longer protects against this, and
mypy rejected the new except syntax because mypy.ini set python_version to
3.13.

Per human decision, embrace the new black style rather than pinning an aging
formatter version:

- mypy.ini: python_version 3.13 -> 3.14, matching the project's Python
  version and the (previously shadowed) [tool.mypy] setting in
  pyproject.toml, so mypy accepts PEP 758 except clauses.
- noxfile.py: drop the now-moot black!=26.1.0 exclusion.
- Apply black 26.5.1 formatting churn: PEP 758 except clause in
  ical_service.py and multiline-string hugging in two integration test
  files.

nox -s format, lint, and type_check all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… document.

Records Milestone 1 progress: regression tests (initially failing, now
passing), the two-stage delete implementation in the API, the
permanent-delete confirmation in the admin UI, the full test pass (671 unit,
173 integration, 133 frontend; format/lint/type_check clean), and the
human-approved black 26.5.x / mypy 3.14 toolchain side quest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 6, 2026 11:07
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
__init__.py100100% 
app.py1383177%35, 75, 193, 235–236, 238–239, 241–244, 256–258, 261, 263, 273, 275, 278–279, 282–285, 289–292, 295–296, 299
database_utils.py17013023%30–36, 41–47, 57, 59–61, 64–65, 67–69, 72–74, 77, 79, 88, 90–93, 95, 116, 118, 121–123, 126, 129, 131–132, 134–135, 137–139, 142–146, 159, 161–164, 167, 177, 186–187, 190, 219, 247, 250, 283–284, 286–287, 289–292, 301, 303, 308, 310, 315, 318–319, 324, 326, 333, 336–337, 339–340, 347–348, 356, 361, 384–387, 390, 393–396, 399–401, 404–406, 408–409, 411–413, 426–429, 431–433, 436, 439, 442, 445–447, 449–450, 452–454
db_resilience.py122397%186, 256, 280
exceptions.py1100100% 
health.py1121685%81–83, 86–87, 98, 119–120, 134, 147, 158–160, 170–172
ical_parser.py113694%85, 120, 124, 127, 134, 197
ical_service.py1701889%80, 82–85, 87, 127–128, 130, 233, 342, 368–369, 375, 382, 454, 456, 460
logging_config.py81396%110, 112, 190
metrics.py236697%426–427, 471–472, 482–483
middleware.py36683%77, 95–96, 109–110, 117
migration_utils.py73730%8–9, 11–12, 15, 18–19, 26–32, 34–35, 46–48, 50–55, 57–58, 68–70, 72–77, 79–80, 90–96, 98–99, 105–108, 110–111, 117–120, 122–123, 133–138, 141, 144, 151, 153–154, 156, 161–162
sse.py1675865%45, 47–48, 50–51, 53–54, 57–59, 61–62, 65–66, 68, 118, 120–122, 204, 210–211, 213–214, 218, 225, 227–228, 230, 232, 234, 309, 322, 324, 332, 335–336, 338, 341, 345–346, 348, 352–353, 355, 358–359, 361, 365, 369–370, 372, 383, 395–398, 449
storage.py3314985%88–90, 156–157, 163, 168, 259–261, 489–491, 512, 526, 573–576, 580, 604–606, 617–619, 675–676, 680, 701–703, 743–746, 774–775, 777–783, 785, 832–833, 860
storage_resilience.py1635367%68–69, 203–204, 211, 213–219, 266–270, 303–305, 334, 347–348, 352–358, 362–364, 382, 384–389, 391–401, 403, 405
validation.py1381787%221, 223, 229, 263, 265–266, 268–269, 337–342, 348, 376, 413
api
   __init__.py10280%27, 38
   helpers.py772074%170, 179, 222, 230, 235, 243, 248, 256, 261, 270, 275, 283–286, 293, 301–304
   v1.py157664559%89, 177–179, 219, 225, 231, 240, 262, 290–292, 304, 341, 345, 361–364, 455, 488, 492, 495, 515–516, 519, 529, 533, 537, 656–658, 661, 675, 679, 683, 687, 752, 755–756, 761–762, 765–766, 768–769, 772–773, 776, 789, 800, 823–826, 842, 846, 850, 876–879, 889, 893, 897, 901, 905, 926, 933–935, 952–957, 975, 979, 998–1002, 1025, 1040–1042, 1057, 1064–1066, 1080, 1089–1091, 1108, 1119–1121, 1131, 1135, 1140, 1145, 1150, 1170–1171, 1187–1190, 1200, 1204, 1208–1210, 1220, 1224, 1228, 1244–1245, 1249, 1262, 1319–1322, 1329–1332, 1334–1336, 1338–1340, 1343–1344, 1347–1350, 1353–1354, 1357, 1365–1366, 1368, 1371, 1382, 1387, 1390, 1394–1398, 1401, 1411, 1415, 1426–1429, 1441–1444, 1446–1448, 1450–1451, 1453–1454, 1456, 1459, 1461–1464, 1471–1474, 1476–1478, 1480–1481, 1483–1485, 1487, 1490, 1492–1495, 1512, 1540–1542, 1549–1551, 1553, 1555–1557, 1576, 1578–1581, 1584, 1587–1592, 1595, 1598, 1600, 1602–1606, 1618–1621, 1623, 1626, 1628, 1630–1632, 1639–1642, 1644–1646, 1649–1650, 1653, 1656–1657, 1660, 1673–1674, 1676, 1679, 1683–1693, 1700–1703, 1705–1707, 1709–1710, 1712, 1714–1716, 1723–1726, 1728–1730, 1732–1733, 1735–1737, 1740, 1742, 1749–1751, 1753–1766, 1768–1769, 1771, 1774, 1776–1786, 1793–1796, 1798–1800, 1802–1803, 1805–1807, 1809, 1812, 1814–1817, 1828–1831, 1833–1835, 1837–1839, 1841–1842, 1844–1845, 1848–1850, 1852, 1855, 1857–1862, 1865, 1872–1875, 1877–1879, 1881–1882, 1884–1887, 1889–1891, 1893–1894, 1897–1899, 1901–1905, 1908, 1910–1911, 1914, 1916–1917, 1919–1920, 1922, 1924, 1928, 1935, 1939–1942, 1984, 2064–2066, 2069–2070, 2078, 2084, 2092, 2101, 2122, 2126, 2156–2158, 2160–2162, 2170, 2180, 2184, 2223–2225, 2227–2229, 2237, 2264, 2272, 2316–2318, 2320–2322, 2324–2326, 2330, 2357, 2377, 2387, 2405–2407, 2409–2411, 2419, 2457–2459, 2467, 2482, 2490, 2514, 2522, 2534–2536, 2546, 2550, 2554, 2559, 2564–2565, 2570, 2586, 2598–2600, 2642, 2650, 2677–2679, 2689, 2700–2701, 2709–2711, 2736, 2738, 2741, 2745, 2747, 2749–2754, 2760, 2762–2764, 2767, 2772–2773, 2775, 2779, 2781–2783, 2799, 2801, 2803, 2807, 2813–2814, 2816–2822, 2825–2826, 2829–2837, 2840–2848, 2850–2851, 2854–2855, 2858–2859, 2862–2868, 2872, 2874, 2876–2877, 2880, 2896, 2950–2952, 2959–2960, 2962–2964, 2966–2968, 2971, 2980, 2982, 2986, 2996–2998, 3021, 3050–3052, 3172–3173, 3180–3181, 3183–3185, 3187–3189, 3191, 3194, 3197, 3200–3202, 3204–3211, 3213–3214, 3216–3221, 3223, 3226–3227, 3229–3230, 3243, 3246–3247, 3251, 3253–3254, 3266–3267, 3271, 3275, 3286–3288, 3291
auth
   __init__.py40100% 
   decorators.py481275%54, 106, 122, 132–134, 136, 148–149, 151, 153–154
   views.py831878%73, 75–77, 120–121, 162–163, 165–166, 172, 240–241, 244–245, 254, 258, 267
cli
   __init__.py10100% 
   init_db.py856721%41–46, 48–49, 52–55, 57–58, 61–62, 65–66, 68, 73, 75–79, 81–82, 85–92, 94, 96, 100, 102–103, 141–142, 145, 148–151, 153–157, 159, 162, 171–173, 176–177, 179–183, 185, 187–188
   main.py351557%17, 28, 30–32, 41, 43–44, 53–54, 56, 58–59, 61, 66
config
   __init__.py580100% 
dashboard
   __init__.py20100% 
   views.py561769%88–91, 151, 153–157, 159, 161, 163, 165, 177, 188–189
display
   __init__.py20100% 
   views.py1514768%31, 33, 38, 53, 171–172, 187, 195, 230–231, 239, 273, 275–276, 278, 280–281, 283, 285, 306–308, 311–312, 314, 325, 333–334, 343, 354, 356–357, 364, 366, 368, 375, 388, 391, 410, 413, 415–416, 423, 425, 427, 433, 444
main
   __init__.py17170%10, 12, 14, 17, 20–21, 30, 33–35, 42, 45, 52, 55–56, 63, 69
models
   __init__.py4794391%126, 141, 263, 405, 407, 414, 431, 559, 584, 592, 594, 601, 609, 617, 725, 792, 799, 827, 885, 890, 924–927, 930, 1096, 1112, 1115, 1117, 1165–1166, 1168–1173, 1179, 1181, 1200, 1203, 1210, 1213
slideshow
   __init__.py503432%43, 46, 52–54, 56–58, 61–64, 66, 73–77, 80–82, 84, 90–91, 97, 103, 105, 107–112, 114
static
   __init__.py00100% 
templates
   __init__.py00100% 
utils
   __init__.py523532%43–46, 49–52, 65, 67–69, 71–72, 75–79, 92–97, 110–111, 121, 135–137, 140, 143–144, 146
TOTAL4956144170% 

Tests Skipped Failures Errors Time
687 1 💤 0 ❌ 0 🔥 57.601s ⏱️

…eshow().

Adds a storage helper used by the upcoming duplicate-slideshow endpoint:
copy_file_to_slideshow() copies an existing uploaded file (identified by its
relative path as stored in SlideshowItem.content_file_path) into another
slideshow's upload directory, generating a fresh secure filename, and returns
the new relative path. This lets a duplicated slideshow own independent
copies of media files rather than referencing the original slideshow's files,
which live in a per-slideshow directory.

Leading "/" and "uploads/" prefixes on the stored path are tolerated to match
the lenient handling in SlideshowItem.display_url. A missing source file or
copy failure logs a warning/error and returns None so callers can degrade
gracefully instead of failing an entire duplicate operation.

Adds unit tests covering the happy path, the uploads/-prefixed path form,
and a missing source file. Full unit suite passes (674 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Implements Milestone 1 of the “Slideshow management fixes” feature by aligning backend delete semantics, frontend UX, and test coverage so inactive slideshow items can be permanently removed (while active items remain soft-deleted on first delete). Also updates the Python 3.14 toolchain configuration (black + mypy target version) and records the feature plan/progress in docs.

Changes:

  • Backend: DELETE /api/v1/slideshow-items/<id> now performs a two-stage delete (active → soft delete, inactive → hard delete) and preserves SSE/audit logging behavior.
  • Frontend: Delete confirmation messaging now warns when an inactive item will be permanently deleted; corresponding unit and Playwright tests added.
  • Tooling/docs: remove stale black exclusion, set mypy to Python 3.14, and add feature specification/progress doc.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/test_api.py Adds unit regression coverage for two-stage delete behavior (inactive hard delete + active-then-inactive flow).
tests/integration/test_slideshow_items.py Adds Playwright coverage for permanently deleting inactive items; includes minor formatting adjustments to page.evaluate calls.
tests/integration/test_display_playback.py Formatting-only adjustments to page.evaluate calls for consistency with updated formatter behavior.
noxfile.py Updates formatter install step to use black without the prior !=26.1.0 exclusion.
mypy.ini Targets mypy at Python 3.14 to match the project/tooling baseline.
kiosk_show_replacement/ical_service.py Formatter-driven update to exception clause formatting compatible with the Python 3.14 target.
kiosk_show_replacement/api/v1.py Implements two-stage deletion semantics for slideshow items and adjusts SSE payload/logging to work for hard deletes.
frontend/src/test/SlideshowDetail.test.tsx Adds frontend unit test ensuring inactive-item delete uses the permanent-delete confirmation message.
frontend/src/pages/SlideshowDetail.tsx Updates delete handler to vary confirmation text based on item active/inactive status.
docs/features/slideshow-management-fixes.md Adds/updates feature spec and progress tracking for slideshow management fixes and upcoming duplication work.

Comment thread frontend/src/test/SlideshowDetail.test.tsx Outdated
jantman and others added 8 commits June 6, 2026 07:13
…licate endpoint.

Adds a duplicate-slideshow API endpoint that creates a copy of a slideshow
under a new caller-supplied name:

- Validates the name exactly like create_slideshow (required, trimmed,
  globally unique among active slideshows, with the unique-constraint
  IntegrityError as backstop) and returns 404 for missing or inactive
  source slideshows.
- Copies description, default_item_duration, and transition_type; the
  duplicate is owned by the current user and is never the default slideshow.
- Copies all items, including inactive ones (which stay inactive),
  preserving title, content fields, display_duration, order_index,
  scale_factor, and iCal settings.
- Uploaded media files are copied into the duplicate's upload directory via
  StorageManager.copy_file_to_slideshow() so the copy is independent of the
  original; a missing source file degrades to an item without a file
  reference rather than failing the duplicate.
- The whole operation is one transaction; on failure, already-copied files
  are cleaned up best-effort and the database is rolled back.

Adds nine unit tests covering settings/item copying (including inactive
items and order preservation), the is_default flag not being copied,
duplicate/missing name validation, 404s for missing and soft-deleted
sources, authentication, on-disk file copying, and missing-file tolerance.
Full unit suite passes (683 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ws admin page.

Adds a Duplicate button (copy icon) to each row's action group on
/admin/slideshows. Clicking it prompts for the new slideshow's name via
window.prompt() (matching the page's existing native-dialog pattern),
pre-filled with "{name} (Copy)" as a convenience. Cancelling the prompt is a
no-op; a blank name shows the page's error alert without calling the API;
API errors (e.g. duplicate name) are surfaced in the same alert. On success
the slideshow list is refreshed.

Supporting changes:
- apiClient.duplicateSlideshow(id, name) calling
  POST /api/v1/slideshows/<id>/duplicate (no-retry, like other mutations).
- useApi hook routes the duplicate endpoint to the new apiClient method.

Adds four frontend unit tests: successful duplicate (prompt pre-fill and API
call), cancelled prompt (no API call), blank name (error alert, no API
call), and API failure (error alert). Full frontend suite passes
(137 passed); type-check and lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… slideshow duplication.

Adds three browser integration tests for the Duplicate action on the
Slideshows admin page:

- test_duplicate_slideshow_success: creates a slideshow with one active and
  one soft-deleted item via the API, duplicates it through the UI (accepting
  the name prompt with a new name), verifies the duplicate appears in the
  list, and confirms via the API that both items were copied with their
  active/inactive states preserved.
- test_duplicate_slideshow_cancel: dismissing the name prompt creates
  nothing.
- test_duplicate_slideshow_duplicate_name_error: accepting the prompt with
  an existing slideshow name surfaces the "already exists" error alert.

Full integration suite passes (176 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in storage helper.

nox -s format reflowed two expressions in the new duplicate API tests, and
pycodestyle flagged E203 (whitespace before ':') on the black-formatted
slice in copy_file_to_slideshow(); replace the startswith/slice pair with
str.removeprefix(), which is clearer and sidesteps the black/pycodestyle
conflict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… document.

Records Milestone 2 progress: the storage copy helper, the duplicate API
endpoint, the admin UI Duplicate action, Playwright integration tests, and
the full test pass (683 unit, 176 integration, 137 frontend;
format/lint/type_check and frontend type-check/lint clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elete and duplication.

docs/api.rst:
- Document the new POST /api/v1/slideshows/<id>/duplicate endpoint (request
  body, 201 response, side effects).
- Update DELETE /api/v1/slideshow-items/<id> to describe the two-stage
  behavior (active items are soft-deleted; inactive items are permanently
  removed).

docs/usage.rst:
- Add a "Duplicating Slideshows" section describing the Duplicate action,
  independent file copies, and the default-slideshow flag not being copied.
- Note in "Managing Slides" that removing a slide deactivates it and that
  deleting an inactive slide is permanent.
- Add "duplicate" to the admin interface feature list.

README.md and CLAUDE.md need no changes: neither enumerates per-action
slideshow capabilities or API endpoints. Sphinx docs build succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…error paths.

Coverage review of the feature's new code found two duplicate-endpoint
paths without tests:

- test_duplicate_slideshow_no_data: a request without a JSON body returns
  400.
- test_duplicate_slideshow_name_conflict_with_inactive: an inactive
  slideshow's name passes the active-only duplicate-name check but violates
  the (name, owner_id) unique constraint at flush time, exercising the
  IntegrityError backstop.
- test_duplicate_slideshow_cleanup_on_failure: failing the final commit
  after items are processed exercises the generic failure handler,
  verifying the response is 500, the copied media file is removed from
  disk, and no duplicate slideshow row is persisted.

The only remaining uncovered line in the new code is the file-cleanup loop
inside the IntegrityError handler, which cannot execute in practice because
the constraint violation surfaces at flush (before any file copies); it is
kept as defense-in-depth. Full unit suite passes (686 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he feature.

Final acceptance-criteria tasks for the Slideshow Management Fixes feature:

- Minor version bump in pyproject.toml: 0.3.5 -> 0.4.0 (two-stage slideshow
  item deletion and the new duplicate-slideshow capability).
- Move the feature document to docs/features/completed/ with Milestone 3
  progress recorded.
- Apply one black reflow in tests/unit/test_api.py from the final
  nox -s format run.

Final verification, all passing: 686 backend unit tests, 176 Playwright
integration tests, 137 frontend tests; nox format, lint, type_check, and
docs sessions clean; frontend type-check, lint, and production build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 6, 2026 11:36

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment thread kiosk_show_replacement/storage.py
jantman and others added 2 commits June 6, 2026 07:50
…al guard and test mock hygiene.

Addresses two Copilot review comments on PR #23:

1. StorageManager.copy_file_to_slideshow() now resolves the source path and
   refuses to copy anything that resolves outside the upload folder. Since
   SlideshowItem.content_file_path is writable via the API, a crafted value
   like "images/../../etc/secret" could previously have copied arbitrary
   server files into the uploads directory (and thus exposed them via
   /uploads/). Adds a unit test verifying traversal attempts return None and
   copy nothing.

2. The frontend tests added by this branch no longer override window.confirm
   / window.prompt by direct assignment, which could leak into later tests
   if an assertion threw before the manual restore. They now use
   vi.spyOn(), and the SlideshowDetail and Slideshows suites gained an
   afterEach(() => vi.restoreAllMocks()) so spies are restored even on test
   failure. Pre-existing tests using the manual save/restore pattern are
   left unchanged.

Backend: 687 unit tests pass. Frontend: 137 tests pass; type-check and lint
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nox -s format collapsed a list comprehension assertion in the new
test_copy_file_to_slideshow_rejects_path_traversal onto one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 6, 2026 11:51

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Comment thread frontend/src/test/Slideshows.test.tsx
Comment thread frontend/src/test/SlideshowDetail.test.tsx
…es to vi.spyOn.

Follow-up to Copilot review feedback on PR #23: the previous commit added
afterEach(() => vi.restoreAllMocks()) to the Slideshows and SlideshowDetail
test suites, but each file still had one pre-existing test mocking
window.confirm by direct assignment, which vi.restoreAllMocks() does not
restore. If such a test failed before its manual cleanup line, the mock
could leak into later tests and make the suites order-dependent.

Convert those two remaining tests ('handles delete slideshow' and 'handles
delete item') to vi.spyOn(window, 'confirm').mockReturnValue(true) and drop
their manual save/restore, so every window.confirm/window.prompt mock in
both suites is now a spy covered by the afterEach restore.

Full frontend suite passes (137 passed); type-check and lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jantman
jantman requested a review from Copilot June 6, 2026 12:30

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

@jantman
jantman merged commit 17eb066 into main Jun 6, 2026
8 checks passed
@jantman
jantman deleted the slideshow-management-fixes branch June 6, 2026 12:33
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