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 062c52eec7
commit 12245759ac
149 changed files with 14846 additions and 92 deletions
+132
View File
@@ -0,0 +1,132 @@
<template>
<div class="approve-card">
<img :src="item.thumbnailUrl" :alt="`Photo from ${item.sharedBy}`" class="approve-card__thumb" loading="lazy" />
<div class="approve-card__body">
<p class="approve-card__from">From <strong>{{ item.sharedBy }}</strong></p>
<p class="approve-card__date">{{ formattedDate }}</p>
<div class="approve-card__status" v-if="item.status !== 'pending'">
<span :class="['approve-card__badge', `approve-card__badge--${item.status}`]">
{{ item.status }}
</span>
</div>
<div class="approve-card__actions">
<template v-if="item.status === 'pending' || item.status === 'declined'">
<BaseButton variant="primary" size="sm" :disabled="busy" @click="showPicker = true">
{{ item.status === 'declined' ? 'Add anyway' : 'Add to frame' }}
</BaseButton>
</template>
<template v-if="item.status === 'pending' || item.status === 'approved'">
<BaseButton variant="ghost" size="sm" :disabled="busy" @click="decline">
{{ item.status === 'approved' ? 'Remove' : 'Decline' }}
</BaseButton>
</template>
</div>
</div>
</div>
<DevicePicker
v-model="showPicker"
:devices="devicesStore.devices"
:selected="selectedDeviceIds"
:uploading="busy"
confirm-label="Add to frames"
@update:selected="selectedDeviceIds = $event"
@confirm="approve"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { SharedImage } from '@/types'
import BaseButton from '@/components/BaseButton.vue'
import DevicePicker from '@/components/DevicePicker.vue'
import { useImagesStore } from '@/stores/images'
import { useDevicesStore } from '@/stores/devices'
const props = defineProps<{ item: SharedImage }>()
const emit = defineEmits<{ (e: 'updated', v: SharedImage): void }>()
const imagesStore = useImagesStore()
const devicesStore = useDevicesStore()
const showPicker = ref(false)
const busy = ref(false)
const selectedDeviceIds = ref<number[]>([])
const formattedDate = computed(() =>
new Date(props.item.sharedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
)
async function approve() {
showPicker.value = false
busy.value = true
try {
const updated = await imagesStore.approveShared(props.item.id, selectedDeviceIds.value)
emit('updated', updated)
} finally {
busy.value = false
selectedDeviceIds.value = []
}
}
async function decline() {
busy.value = true
try {
const updated = await imagesStore.declineShared(props.item.id)
emit('updated', updated)
} finally {
busy.value = false
}
}
</script>
<style scoped lang="scss">
.approve-card {
display: flex;
gap: var(--space-3);
padding: var(--space-3);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
&__thumb {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: var(--radius-sm);
flex-shrink: 0;
background: var(--color-border);
}
&__body {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
&__from { font-size: var(--text-sm); }
&__date { font-size: var(--text-xs); color: var(--color-text-muted); }
&__badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: var(--text-xs);
font-weight: 600;
text-transform: capitalize;
&--approved { background: #d4edda; color: #1a7f4b; }
&--declined { background: #fde8e8; color: #d93025; }
}
&__actions {
display: flex;
gap: var(--space-2);
margin-top: auto;
flex-wrap: wrap;
}
}
</style>