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
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260505040613 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add image, rendered_asset, image_device_approval tables; add model column to device';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE image (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, original_filename VARCHAR(255) NOT NULL, storage_path VARCHAR(500) NOT NULL, uploaded_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, deleted_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, user_id INT NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_C53D045FA76ED395 ON image (user_id)');
$this->addSql('CREATE TABLE image_device_approval (image_id INT NOT NULL, device_id INT NOT NULL, PRIMARY KEY (image_id, device_id))');
$this->addSql('CREATE INDEX IDX_3524D29A3DA5256D ON image_device_approval (image_id)');
$this->addSql('CREATE INDEX IDX_3524D29A94A4C7D4 ON image_device_approval (device_id)');
$this->addSql('CREATE TABLE rendered_asset (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, device_model VARCHAR(255) NOT NULL, orientation VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, file_path VARCHAR(500) DEFAULT NULL, rendered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, image_id INT NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_DF34C8E33DA5256D ON rendered_asset (image_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_DF34C8E33DA5256D111092BE3680C556 ON rendered_asset (image_id, device_model, orientation)');
$this->addSql('ALTER TABLE image ADD CONSTRAINT FK_C53D045FA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE image_device_approval ADD CONSTRAINT FK_3524D29A3DA5256D FOREIGN KEY (image_id) REFERENCES image (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE image_device_approval ADD CONSTRAINT FK_3524D29A94A4C7D4 FOREIGN KEY (device_id) REFERENCES device (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE rendered_asset ADD CONSTRAINT FK_DF34C8E33DA5256D FOREIGN KEY (image_id) REFERENCES image (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql("ALTER TABLE device ADD model VARCHAR(255) NOT NULL DEFAULT 'v1'");
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE image DROP CONSTRAINT FK_C53D045FA76ED395');
$this->addSql('ALTER TABLE image_device_approval DROP CONSTRAINT FK_3524D29A3DA5256D');
$this->addSql('ALTER TABLE image_device_approval DROP CONSTRAINT FK_3524D29A94A4C7D4');
$this->addSql('ALTER TABLE rendered_asset DROP CONSTRAINT FK_DF34C8E33DA5256D');
$this->addSql('DROP TABLE image');
$this->addSql('DROP TABLE image_device_approval');
$this->addSql('DROP TABLE rendered_asset');
$this->addSql('ALTER TABLE device DROP COLUMN model');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260505120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add wake_hour to device for time-based wake scheduling';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE device ADD wake_hour INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE device DROP COLUMN wake_hour');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260505130000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add timezone to user';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" ADD timezone VARCHAR(60) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" DROP COLUMN timezone');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260505140000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add timezone to device (per-device scheduling context)';
}
public function up(Schema $schema): void
{
$this->addSql("ALTER TABLE device ADD timezone VARCHAR(60) NOT NULL DEFAULT 'UTC'");
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE device DROP COLUMN timezone');
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260505150000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add crop_params and sticker_state to image for re-edit support';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE image ADD crop_params TEXT DEFAULT NULL');
$this->addSql('ALTER TABLE image ADD sticker_state TEXT DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE image DROP COLUMN crop_params');
$this->addSql('ALTER TABLE image DROP COLUMN sticker_state');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260506000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add device_image_history table for rotation uniqueness tracking';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE device_image_history (
id SERIAL NOT NULL,
device_id INT NOT NULL,
image_id INT NOT NULL,
served_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY(id)
)');
$this->addSql('CREATE INDEX idx_history_device_served ON device_image_history (device_id, served_at)');
$this->addSql('ALTER TABLE device_image_history ADD CONSTRAINT fk_history_device FOREIGN KEY (device_id) REFERENCES device (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE device_image_history ADD CONSTRAINT fk_history_image FOREIGN KEY (image_id) REFERENCES image (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('COMMENT ON COLUMN device_image_history.served_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE device_image_history DROP CONSTRAINT fk_history_device');
$this->addSql('ALTER TABLE device_image_history DROP CONSTRAINT fk_history_image');
$this->addSql('DROP TABLE device_image_history');
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260506010000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add last_seen_at to device';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE device ADD last_seen_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN device.last_seen_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE device DROP COLUMN last_seen_at');
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260506020000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add current_image_id FK to device';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE device ADD current_image_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE device ADD CONSTRAINT fk_device_current_image FOREIGN KEY (current_image_id) REFERENCES image (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_device_current_image ON device (current_image_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE device DROP CONSTRAINT fk_device_current_image');
$this->addSql('DROP INDEX idx_device_current_image');
$this->addSql('ALTER TABLE device DROP COLUMN current_image_id');
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260506200000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Rename rotation_interval_hours to rotation_interval_minutes (1 hour = 60 minutes)';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE device RENAME COLUMN rotation_interval_hours TO rotation_interval_minutes');
$this->addSql('ALTER TABLE device ALTER COLUMN rotation_interval_minutes SET DEFAULT 1440');
$this->addSql('UPDATE device SET rotation_interval_minutes = rotation_interval_minutes * 60');
}
public function down(Schema $schema): void
{
$this->addSql('UPDATE device SET rotation_interval_minutes = rotation_interval_minutes / 60');
$this->addSql('ALTER TABLE device ALTER COLUMN rotation_interval_minutes SET DEFAULT 24');
$this->addSql('ALTER TABLE device RENAME COLUMN rotation_interval_minutes TO rotation_interval_hours');
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260506210000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add locked_image_id to device for pinning an image and bypassing rotation';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE device ADD COLUMN locked_image_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE device ADD CONSTRAINT fk_device_locked_image FOREIGN KEY (locked_image_id) REFERENCES image(id) ON DELETE SET NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE device DROP CONSTRAINT fk_device_locked_image');
$this->addSql('ALTER TABLE device DROP COLUMN locked_image_id');
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260507100000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add token table for share and hard-delete flows';
}
public function up(Schema $schema): void
{
$this->addSql("CREATE TABLE token (
uuid VARCHAR(36) NOT NULL,
type VARCHAR(30) NOT NULL,
image_id INT NOT NULL,
recipient_user_id INT DEFAULT NULL,
recipient_email VARCHAR(180) DEFAULT NULL,
expires_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
used_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
PRIMARY KEY(uuid)
)");
$this->addSql("COMMENT ON COLUMN token.expires_at IS '(DC2Type:datetime_immutable)'");
$this->addSql("COMMENT ON COLUMN token.used_at IS '(DC2Type:datetime_immutable)'");
$this->addSql('ALTER TABLE token ADD CONSTRAINT fk_token_image FOREIGN KEY (image_id) REFERENCES image (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE token ADD CONSTRAINT fk_token_recipient FOREIGN KEY (recipient_user_id) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_token_recipient ON token (recipient_user_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE token DROP CONSTRAINT fk_token_image');
$this->addSql('ALTER TABLE token DROP CONSTRAINT fk_token_recipient');
$this->addSql('DROP TABLE token');
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260507200000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add shared_image table for family sharing';
}
public function up(Schema $schema): void
{
$this->addSql("CREATE TABLE shared_image (
id SERIAL NOT NULL,
source_image_id INT NOT NULL,
recipient_user_id INT NOT NULL,
shared_by_id INT NOT NULL,
shared_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
PRIMARY KEY(id)
)");
$this->addSql("COMMENT ON COLUMN shared_image.shared_at IS '(DC2Type:datetime_immutable)'");
$this->addSql('CREATE INDEX idx_shared_recipient_status ON shared_image (recipient_user_id, status)');
$this->addSql('ALTER TABLE shared_image ADD CONSTRAINT fk_shared_source FOREIGN KEY (source_image_id) REFERENCES image (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE shared_image ADD CONSTRAINT fk_shared_recipient FOREIGN KEY (recipient_user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE shared_image ADD CONSTRAINT fk_shared_by FOREIGN KEY (shared_by_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE shared_image DROP CONSTRAINT fk_shared_source');
$this->addSql('ALTER TABLE shared_image DROP CONSTRAINT fk_shared_recipient');
$this->addSql('ALTER TABLE shared_image DROP CONSTRAINT fk_shared_by');
$this->addSql('DROP TABLE shared_image');
}
}