Feat: Replace UUID image identifiers with 8-character base62 short IDs

Short IDs become the canonical identifier in URLs (/i/:short_id),
MinIO/R2 storage keys, and all API responses. Hash-based deduplication
is preserved. Includes two-phase Alembic migration (003 adds nullable
column, 004 enforces NOT NULL) with a backfill script to copy storage
objects and populate short_id for existing images.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 00:13:55 +00:00
parent 87eb2703f5
commit 61d923d5be
41 changed files with 1445 additions and 137 deletions

View File

@@ -37,9 +37,9 @@ describe('UploadComponent', () => {
component = fixture.componentInstance;
const router = TestBed.inject(Router);
spyOn(router, 'navigate');
await component.handleUploadResponse({ id: 'abc', duplicate: true } as Parameters<typeof component.handleUploadResponse>[0]);
await component.handleUploadResponse({ id: 'abc', short_id: 'AbCd1234', duplicate: true } as Parameters<typeof component.handleUploadResponse>[0]);
expect(component.toastMessage).toContain('library');
expect(router.navigate).toHaveBeenCalledWith(['/images', 'abc']);
expect(router.navigate).toHaveBeenCalledWith(['/i', 'AbCd1234']);
});
it('on success response: shows success toast and navigates to detail', async () => {
@@ -47,9 +47,9 @@ describe('UploadComponent', () => {
component = fixture.componentInstance;
const router = TestBed.inject(Router);
spyOn(router, 'navigate');
await component.handleUploadResponse({ id: 'xyz', duplicate: false } as Parameters<typeof component.handleUploadResponse>[0]);
await component.handleUploadResponse({ id: 'xyz', short_id: 'XyZ12345', duplicate: false } as Parameters<typeof component.handleUploadResponse>[0]);
expect(component.toastMessage).toBeTruthy();
expect(router.navigate).toHaveBeenCalledWith(['/images', 'xyz']);
expect(router.navigate).toHaveBeenCalledWith(['/i', 'XyZ12345']);
});
it('on error response: shows inline error, no navigation', async () => {