feat(setup): post-link redirects to SPA so first-setup matches live UI
CI / test (push) Has been cancelled

Twig configure page replaced with a redirect: SetupController's index,
register, login, and the legacy /configure route all post-link redirect
to /?setup=<deviceId> for unconfigured devices. The SPA's HomeView
auto-opens its existing settings sheet for that id, with the same
controls everyone uses for live edits — themed to the user's choice,
pre-populated from the device record.

Fixes Matt's report:
  - "every 6 hours" lost on save: the configure form posted
    rotation_interval_hours but the controller read
    rotation_interval_minutes, so the value silently defaulted to
    1440 every time. Now the SPA's PATCH flow handles it correctly.
  - "old settings still there in live settings": SPA settings sheet
    pre-populates from the device's current state via onEdit.
  - "uniqueness window in setup but not live settings": removed
    from the (now-deleted) Twig form; both surfaces are consistent.
  - "color scheme didn't match account": SPA respects the user's
    theme natively (data-theme on <html>), so the first-setup screen
    looks like the rest of the app.

Also adds a "Sign out of pictureFrame" link at the bottom of the
per-frame settings sheet (the existing /settings tab still has the
primary one). Easy escape hatch from a deeply-nested settings flow.

Tests:
  - SetupControllerTest: S-03/04/05/06/08 updated for new redirect
    targets, S-CLAIM-03 updated.
  - HomeView.test.ts: useRoute now mockable per-test, two new cases
    pinning the ?setup=<id> auto-open and its absence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 18:51:31 -04:00
parent ff1ae79824
commit 08d0968af0
14 changed files with 139 additions and 95 deletions
+25 -35
View File
@@ -39,10 +39,7 @@ class SetupController extends AbstractController
$user = $this->getUser();
if (!$deviceService->isClaimedByAnotherUser($mac, $user)) {
$device = $deviceService->linkToUser($mac, $user);
if (empty($device->getName())) {
return $this->redirectToRoute('setup_configure', ['mac' => $mac]);
}
return $this->redirectToRoute('spa');
return $this->postLinkRedirect($device);
}
}
@@ -94,7 +91,7 @@ class SetupController extends AbstractController
$allowClaim = $request->request->getBoolean('claim_device');
try {
$deviceService->linkToUser($mac, $user, $allowClaim);
$device = $deviceService->linkToUser($mac, $user, $allowClaim);
} catch (DeviceClaimRequiredException) {
// New account just created and claim wasn't acknowledged.
// Bounce back through the setup page; the checkbox will be
@@ -108,7 +105,7 @@ class SetupController extends AbstractController
return $this->redirectToRoute('setup_index', ['mac' => $mac]);
}
return $this->redirectToRoute('setup_configure', ['mac' => $mac]);
return $this->postLinkRedirect($device);
}
$existing = $em->getRepository(Device::class)->findOneBy(['mac' => strtoupper($mac)]);
@@ -138,7 +135,7 @@ class SetupController extends AbstractController
$security->login($user, 'form_login', 'main');
$allowClaim = $request->request->getBoolean('claim_device');
try {
$deviceService->linkToUser($mac, $user, $allowClaim);
$device = $deviceService->linkToUser($mac, $user, $allowClaim);
} catch (DeviceClaimRequiredException) {
$request->getSession()->set(
'_setup_claim_error',
@@ -146,50 +143,43 @@ class SetupController extends AbstractController
);
return $this->redirectToRoute('setup_index', ['mac' => $mac]);
}
return $this->redirectToRoute('setup_configure', ['mac' => $mac]);
return $this->postLinkRedirect($device);
}
$request->getSession()->set('_setup_login_error', 'Incorrect email or password');
return $this->redirectToRoute('setup_index', ['mac' => $mac]);
}
/**
* Legacy route — first-time configuration now happens in the SPA's
* settings sheet (auto-opens via the ?setup=<id> query param) so users
* see the same controls as live editing, with their theme applied,
* pre-populated from the device's current state. Kept as a redirect so
* any in-flight bookmarks land in the right place.
*/
#[Route('/configure', name: 'setup_configure', methods: ['GET', 'POST'])]
#[IsGranted('ROLE_USER')]
public function configure(
string $mac,
Request $request,
EntityManagerInterface $em,
DeviceService $deviceService,
): Response {
/** @var User $user */
$user = $this->getUser();
$device = $deviceService->linkToUser($mac, $user);
return $this->postLinkRedirect($device);
}
if ($request->isMethod('POST')) {
$name = trim((string) $request->request->get('name', ''));
$orient = $request->request->get('orientation', Orientation::Landscape->value);
$interval = (int) $request->request->get('rotation_interval_minutes', 1440);
$window = (int) $request->request->get('uniqueness_window', 10);
if (empty($name)) {
return $this->render('setup/configure.html.twig', [
'device' => $device,
'error' => 'Please enter a name for your frame.',
]);
}
$device->setName($name);
$device->setOrientation(Orientation::from($orient));
$device->setRotationIntervalMinutes(max(1, $interval));
$device->setUniquenessWindow(max(1, $window));
$em->flush();
return $this->redirectToRoute('spa');
/**
* After linkToUser, hand off to the SPA. If the device hasn't been
* named yet (= first-time setup), append ?setup=<id> so the SPA
* auto-opens its settings sheet for that device. Avoids duplicating
* the rich settings UI in a plain-Twig setup page.
*/
private function postLinkRedirect(Device $device): Response
{
if (empty($device->getName())) {
return $this->redirect('/?setup=' . $device->getId());
}
return $this->render('setup/configure.html.twig', [
'device' => $device,
'error' => null,
]);
return $this->redirectToRoute('spa');
}
}