chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled

Web app: new entities (Image, RenderedAsset, SharedImage, Token,
DeviceImageHistory), enums, repositories, controllers, message handlers,
migrations, tests, frontend upload/library/sticker UI, Vue components.

Firmware: EPD background screen binaries + gen scripts, setup_bg header.

Infra: ddev config, test bundle, gitignore coverage dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:11:31 -04:00
parent dd0970ed7c
commit 4002ff9fbf
156 changed files with 27333 additions and 92 deletions
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="font-family:system-ui,sans-serif;background:#fdf6ee;margin:0;padding:2rem 1rem">
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;border:1px solid #e8d9c4">
<div style="padding:1.5rem">
<p style="color:#8a7060;font-size:.875rem;margin-bottom:.5rem">pictureFrame</p>
<h1 style="font-size:1.25rem;font-weight:700;color:#3a2e22;margin-bottom:.5rem">{{ sharer.email }} shared a photo with you</h1>
<p style="color:#8a7060;font-size:.875rem;margin-bottom:1.25rem">Add it to your frame or decline — it's up to you.</p>
</div>
<img src="{{ absolute_url('/api/images/' ~ image.id ~ '/thumbnail') }}"
alt="Shared photo" width="480"
style="display:block;width:100%;max-width:480px;height:auto">
<div style="padding:1.5rem;display:flex;flex-direction:column;gap:.75rem">
<a href="{{ absolute_url('/token/' ~ approveToken.uuid ~ '/approve') }}"
style="display:block;padding:1rem;background:#c97c3a;color:#fff;border-radius:10px;text-align:center;text-decoration:none;font-weight:700;font-size:1rem">
Add to my frame
</a>
<a href="{{ absolute_url('/token/' ~ declineToken.uuid ~ '/decline?back=' ~ approveToken.uuid) }}"
style="display:block;padding:1rem;background:transparent;color:#8a7060;border:1.5px solid #e8d9c4;border-radius:10px;text-align:center;text-decoration:none;font-weight:600;font-size:.9rem">
No thanks
</a>
</div>
<div style="padding:1rem 1.5rem;border-top:1px solid #e8d9c4;font-size:.75rem;color:#aaa">
These links expire in {{ (approveToken.expiresAt.timestamp - 'now'|date('U')) // 86400 }} days.
</div>
</div>
</body>
</html>
@@ -0,0 +1,9 @@
{{ sharer.email }} shared a photo with you on pictureFrame.
Add to my frame:
{{ absolute_url('/token/' ~ approveToken.uuid ~ '/approve') }}
No thanks (decline):
{{ absolute_url('/token/' ~ declineToken.uuid ~ '/decline?back=' ~ approveToken.uuid) }}
These links expire in {{ (approveToken.expiresAt.timestamp - 'now'|date('U')) // 86400 }} days.
+6 -5
View File
@@ -90,12 +90,13 @@
<div class="field">
<label for="rotation_interval_hours">Rotation frequency</label>
{% set currentHours = device.rotationIntervalMinutes / 60 %}
<select id="rotation_interval_hours" name="rotation_interval_hours">
<option value="6" {% if device.rotationIntervalHours == 6 %}selected{% endif %}>Every 6 hours</option>
<option value="12" {% if device.rotationIntervalHours == 12 %}selected{% endif %}>Every 12 hours</option>
<option value="24" {% if device.rotationIntervalHours == 24 %}selected{% endif %}>Daily (every 24 hours)</option>
<option value="48" {% if device.rotationIntervalHours == 48 %}selected{% endif %}>Every 2 days</option>
<option value="168" {% if device.rotationIntervalHours == 168 %}selected{% endif %}>Weekly</option>
<option value="6" {% if currentHours == 6 %}selected{% endif %}>Every 6 hours</option>
<option value="12" {% if currentHours == 12 %}selected{% endif %}>Every 12 hours</option>
<option value="24" {% if currentHours == 24 %}selected{% endif %}>Daily (every 24 hours)</option>
<option value="48" {% if currentHours == 48 %}selected{% endif %}>Every 2 days</option>
<option value="168" {% if currentHours == 168 %}selected{% endif %}>Weekly</option>
</select>
</div>
+63
View File
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add photo to your frame — pictureFrame</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;min-height:100dvh;background:#fdf6ee;color:#3a2e22;display:flex;align-items:flex-start;justify-content:center;padding:2rem 1rem}
.card{width:100%;max-width:420px}
h1{font-size:1.4rem;font-weight:700;margin-bottom:.25rem}
.sub{font-size:.875rem;color:#8a7060;margin-bottom:1.5rem}
.thumb{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:10px;margin-bottom:1.5rem;background:#e8d9c4}
.label{display:block;font-size:.8125rem;font-weight:600;color:#8a7060;margin-bottom:.75rem}
.devices{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1.5rem}
.device-row{display:flex;align-items:center;gap:.75rem;padding:.75rem;border:1.5px solid #e8d9c4;border-radius:8px;cursor:pointer}
.device-row input{width:18px;height:18px;accent-color:#c97c3a;flex-shrink:0}
.device-name{font-weight:600}
.device-orientation{font-size:.8125rem;color:#8a7060;margin-left:auto}
.btn{display:block;width:100%;padding:.875rem;border:none;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;text-align:center;text-decoration:none;margin-bottom:.75rem}
.btn-primary{background:#c97c3a;color:#fff}
.btn-primary:disabled{opacity:.5;cursor:default}
.btn-ghost{background:transparent;border:1.5px solid #e8d9c4;color:#8a7060}
.login-cta{background:#f5ede2;border-radius:10px;padding:1.25rem;text-align:center;margin-bottom:1rem}
.login-cta p{font-size:.875rem;color:#8a7060;margin-bottom:.75rem}
.login-cta a{color:#c97c3a;font-weight:700;text-decoration:none}
</style>
</head>
<body>
<div class="card">
<h1>Someone shared a photo with you</h1>
<p class="sub">{{ token.image.originalFilename }} — shared by {{ token.recipientEmail }}</p>
<img class="thumb" src="/api/images/{{ token.image.id }}/thumbnail" alt="Shared photo">
{% if user %}
<form method="post">
<label class="label">Add to your frames:</label>
{% if devices is empty %}
<p class="sub">You don't have any frames set up yet. <a href="/" style="color:#c97c3a">Set one up first</a>.</p>
{% else %}
<div class="devices">
{% for device in devices %}
<label class="device-row">
<input type="checkbox" name="device_ids[]" value="{{ device.id }}">
<span class="device-name">{{ device.name }}</span>
<span class="device-orientation">{{ device.orientation }}</span>
</label>
{% endfor %}
</div>
<button class="btn btn-primary" type="submit">Add to selected frames</button>
{% endif %}
</form>
{% else %}
<div class="login-cta">
<p>Log in to add this photo to your frame.</p>
<a href="/login?_target_path=/token/{{ token.uuid }}/approve" class="btn btn-primary" style="display:inline-block;padding:.75rem 1.5rem;border-radius:8px">Log in</a>
</div>
{% endif %}
</div>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photo added — pictureFrame</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;min-height:100dvh;background:#fdf6ee;color:#3a2e22;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}
.card{width:100%;max-width:420px;text-align:center}
.icon{font-size:3rem;margin-bottom:1rem}
h1{font-size:1.4rem;font-weight:700;margin-bottom:.5rem}
p{color:#8a7060;font-size:.875rem;margin-bottom:1.5rem}
a{display:inline-block;padding:.75rem 1.5rem;background:#c97c3a;color:#fff;border-radius:10px;text-decoration:none;font-weight:700}
</style>
</head>
<body>
<div class="card">
<div class="icon">✓</div>
<h1>Photo added to your frame</h1>
<p>It'll appear in your rotation once it's been processed.</p>
<a href="/">Go to your frames</a>
</div>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Decline photo — pictureFrame</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;min-height:100dvh;background:#fdf6ee;color:#3a2e22;display:flex;align-items:flex-start;justify-content:center;padding:2rem 1rem}
.card{width:100%;max-width:420px}
h1{font-size:1.4rem;font-weight:700;margin-bottom:.25rem}
.sub{font-size:.875rem;color:#8a7060;margin-bottom:1.5rem}
.thumb{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:10px;margin-bottom:1.5rem;background:#e8d9c4}
.btn{display:block;width:100%;padding:.875rem;border:none;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;text-align:center;text-decoration:none;margin-bottom:.75rem}
.btn-danger{background:#d93025;color:#fff}
.btn-ghost{background:transparent;border:1.5px solid #e8d9c4;color:#8a7060}
</style>
</head>
<body>
<div class="card">
<h1>Decline this photo?</h1>
<p class="sub">It won't be added to any of your frames.</p>
<img class="thumb" src="/api/images/{{ token.image.id }}/thumbnail" alt="Shared photo">
<form method="post">
<button class="btn btn-danger" type="submit">Yes, decline</button>
</form>
{% if approveUuid %}
<a class="btn btn-ghost" href="/token/{{ approveUuid }}/approve">Actually, I want to add it</a>
{% endif %}
</div>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photo declined — pictureFrame</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;min-height:100dvh;background:#fdf6ee;color:#3a2e22;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}
.card{width:100%;max-width:420px;text-align:center}
h1{font-size:1.4rem;font-weight:700;margin-bottom:.5rem}
p{color:#8a7060;font-size:.875rem}
</style>
</head>
<body>
<div class="card">
<h1>Photo declined</h1>
<p>It won't appear on your frames.</p>
</div>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Link invalid — pictureFrame</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;min-height:100dvh;background:#fdf6ee;color:#3a2e22;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}
.card{width:100%;max-width:420px;text-align:center}
h1{font-size:1.4rem;font-weight:700;margin-bottom:.5rem}
p{color:#8a7060;font-size:.875rem}
</style>
</head>
<body>
<div class="card">
<h1>This link is no longer valid</h1>
<p>{{ reason }}</p>
</div>
</body>
</html>