From f22d5f85a8bcb98d2ec7401871765d9b32544ff1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 19 Apr 2025 15:00:24 -0500 Subject: [PATCH] Consolidate outdated/needs AI text sync statuses --- app/admin/photos/page.tsx | 5 +- app/admin/photos/sync/page.tsx | 4 +- src/admin/AdminPhotoMenu.tsx | 2 +- src/admin/AdminPhotosClient.tsx | 8 +- src/admin/AdminPhotosSyncClient.tsx | 4 +- src/admin/insights/AdminAppInsights.tsx | 10 +-- src/admin/insights/AdminAppInsightsClient.tsx | 10 +-- src/admin/insights/index.ts | 18 ++-- src/admin/insights/server.ts | 8 +- src/components/CopyButton.tsx | 20 ++--- src/components/primitives/LoaderButton.tsx | 56 ++++++++----- src/photo/db/query.ts | 83 +++++++------------ src/photo/index.ts | 14 ++-- src/photo/sync.ts | 18 ++-- 14 files changed, 123 insertions(+), 137 deletions(-) diff --git a/app/admin/photos/page.tsx b/app/admin/photos/page.tsx index 5749df9b..7b367166 100644 --- a/app/admin/photos/page.tsx +++ b/app/admin/photos/page.tsx @@ -1,11 +1,10 @@ import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache'; -import { getPhotos } from '@/photo/db/query'; +import { getPhotos, getPhotosInNeedOfSyncCount } from '@/photo/db/query'; import { getPhotosMetaCached } from '@/photo/cache'; import AdminPhotosClient from '@/admin/AdminPhotosClient'; import { revalidatePath } from 'next/cache'; import { cookies } from 'next/headers'; import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone'; -import { getOutdatedPhotosCount } from '@/photo/db/query'; import { AI_TEXT_GENERATION_ENABLED, PRESERVE_ORIGINAL_UPLOADS, @@ -35,7 +34,7 @@ export default async function AdminPhotosPage() { getPhotosMetaCached({ hidden: 'include'}) .then(({ count }) => count) .catch(() => 0), - getOutdatedPhotosCount() + getPhotosInNeedOfSyncCount() .catch(() => 0), DEBUG_PHOTO_BLOBS ? getStoragePhotoUrlsNoStore() diff --git a/app/admin/photos/sync/page.tsx b/app/admin/photos/sync/page.tsx index 2b103ce0..877a4f02 100644 --- a/app/admin/photos/sync/page.tsx +++ b/app/admin/photos/sync/page.tsx @@ -1,11 +1,11 @@ import AdminPhotosSyncClient from '@/admin/AdminPhotosSyncClient'; import { AI_TEXT_GENERATION_ENABLED } from '@/app/config'; -import { getOutdatedPhotos } from '@/photo/db/query'; +import { getPhotosInNeedOfSync } from '@/photo/db/query'; export const maxDuration = 60; export default async function AdminSyncPage() { - const photos = await getOutdatedPhotos() + const photos = await getPhotosInNeedOfSync() .catch(() => []); return ( diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index c8c69221..99a71b62 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -78,7 +78,7 @@ export default function AdminPhotoMenu({ label: 'Sync', labelComplex: Sync - {photo.needsSync && + {(photo.syncStatus.isOutdated || photo.syncStatus.isMissingAiText) && } - // TODO: Add tooltip - // TODO: Use LinkWithStatus - title={`${photosCountOutdated} Outdated Photos`} + tooltip={( + pluralize(photosCountOutdated, 'photo') + + ' needs sync' + )} className={clsx( 'text-blue-600 dark:text-blue-400', 'border border-blue-200 dark:border-blue-800/60', diff --git a/src/admin/AdminPhotosSyncClient.tsx b/src/admin/AdminPhotosSyncClient.tsx index dee1f686..59e19095 100644 --- a/src/admin/AdminPhotosSyncClient.tsx +++ b/src/admin/AdminPhotosSyncClient.tsx @@ -67,9 +67,7 @@ export default function AdminPhotosSyncClient({ > {arePhotoIdsSyncing ? 'Syncing' - : - Sync Next {updateBatchSize} Photos - } + : 'Sync All'} } >
diff --git a/src/admin/insights/AdminAppInsights.tsx b/src/admin/insights/AdminAppInsights.tsx index de07d2e9..d53a00dd 100644 --- a/src/admin/insights/AdminAppInsights.tsx +++ b/src/admin/insights/AdminAppInsights.tsx @@ -9,13 +9,13 @@ import { } from '@/photo/db/query'; import AdminAppInsightsClient from './AdminAppInsightsClient'; import { getAllInsights, getGitHubMetaForCurrentApp } from '.'; -import { getOutdatedPhotosCount } from '@/photo/db/query'; +import { getPhotosInNeedOfSyncCount } from '@/photo/db/query'; export default async function AdminAppInsights() { const [ { count: photosCount, dateRange }, { count: photosCountHidden }, - photosCountOutdated, + photosCountNeedSync, { count: photosCountPortrait }, codeMeta, cameras, @@ -27,7 +27,7 @@ export default async function AdminAppInsights() { ] = await Promise.all([ getPhotosMeta({ hidden: 'include' }), getPhotosMeta({ hidden: 'only' }), - getOutdatedPhotosCount(), + getPhotosInNeedOfSyncCount(), getPhotosMeta({ maximumAspectRatio: 0.9 }), getGitHubMetaForCurrentApp(), getUniqueCameras(), @@ -44,14 +44,14 @@ export default async function AdminAppInsights() { insights={getAllInsights({ codeMeta, photosCount, - photosCountOutdated, + photosCountNeedSync, photosCountPortrait, tagsCount: tags.length, })} photoStats={{ photosCount, photosCountHidden, - photosCountOutdated, + photosCountNeedSync, camerasCount: cameras.length, lensesCount: lenses.length, tagsCount: tags.length, diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index 45e73690..01969d8e 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -50,7 +50,7 @@ import { HiOutlineDocumentText } from 'react-icons/hi'; const DEBUG_COMMIT_SHA = '4cd29ed'; const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes'; const DEBUG_BEHIND_BY = 9; -const DEBUG_PHOTOS_COUNT_OUTDATED = 7; +const DEBUG_PHOTOS_NEED_SYNC_COUNT = 7; const TEXT_COLOR_WARNING = 'text-amber-600 dark:text-amber-500'; const TEXT_COLOR_BLUE = 'text-blue-600 dark:text-blue-500'; @@ -91,7 +91,7 @@ export default function AdminAppInsightsClient({ photoStats: { photosCount, photosCountHidden, - photosCountOutdated, + photosCountNeedSync, camerasCount, lensesCount, tagsCount, @@ -114,7 +114,7 @@ export default function AdminAppInsightsClient({ noAiRateLimiting, noConfiguredDomain, noConfiguredMeta, - outdatedPhotos, + photosNeedSync, photoMatting, camerasFirst, gridFirst, @@ -417,7 +417,7 @@ export default function AdminAppInsightsClient({ } - {(outdatedPhotos || debug) && } content={renderHighlightText( pluralize( - photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED, + photosCountNeedSync || DEBUG_PHOTOS_NEED_SYNC_COUNT, 'photo', ) + ' need to be synced', 'blue', diff --git a/src/admin/insights/index.ts b/src/admin/insights/index.ts index 65b22eb3..7837ea14 100644 --- a/src/admin/insights/index.ts +++ b/src/admin/insights/index.ts @@ -39,7 +39,7 @@ const _INSIGHTS_TEMPLATE = [ type AdminAppInsightRecommendation = typeof _INSIGHTS_TEMPLATE[number]; const _INSIGHTS_LIBRARY = [ - 'outdatedPhotos', + 'photosNeedSync', ] as const; type AdminAppInsightLibrary = typeof _INSIGHTS_LIBRARY[number]; @@ -58,7 +58,7 @@ export const hasTemplateRecommendations = (insights: AdminAppInsights) => export interface PhotoStats { photosCount: number photosCountHidden: number - photosCountOutdated: number + photosCountNeedSync: number camerasCount: number lensesCount: number tagsCount: number @@ -80,10 +80,10 @@ export const getGitHubMetaForCurrentApp = () => export const getSignificantInsights = ({ codeMeta, - photosCountOutdated, + photosCountNeedSync, }: { codeMeta: Awaited> - photosCountOutdated: number + photosCountNeedSync: number }) => { const { isAiTextGenerationEnabled, @@ -95,7 +95,7 @@ export const getSignificantInsights = ({ forkBehind: Boolean(codeMeta?.isBehind), noAiRateLimiting: isAiTextGenerationEnabled && !hasRedisStorage, noConfiguredDomain: !hasDomain, - outdatedPhotos: Boolean(photosCountOutdated), + photosNeedSync: Boolean(photosCountNeedSync), }; }; @@ -106,19 +106,19 @@ export const indicatorStatusForSignificantInsights = ( forkBehind, noAiRateLimiting, noConfiguredDomain, - outdatedPhotos, + photosNeedSync, } = insights; if (noAiRateLimiting || noConfiguredDomain) { return 'yellow'; - } else if (forkBehind || outdatedPhotos) { + } else if (forkBehind || photosNeedSync) { return 'blue'; } }; export const getAllInsights = ({ codeMeta, - photosCountOutdated, + photosCountNeedSync, photosCount, photosCountPortrait, tagsCount, @@ -127,7 +127,7 @@ export const getAllInsights = ({ photosCountPortrait: number tagsCount: number }) => ({ - ...getSignificantInsights({ codeMeta, photosCountOutdated }), + ...getSignificantInsights({ codeMeta, photosCountNeedSync }), noFork: !codeMeta?.isForkedFromBase && !codeMeta?.isBaseRepo, noAi: !AI_TEXT_GENERATION_ENABLED, noConfiguredMeta: diff --git a/src/admin/insights/server.ts b/src/admin/insights/server.ts index 3b453def..168c167b 100644 --- a/src/admin/insights/server.ts +++ b/src/admin/insights/server.ts @@ -1,4 +1,4 @@ -import { getOutdatedPhotosCount } from '@/photo/db/query'; +import { getPhotosInNeedOfSyncCount } from '@/photo/db/query'; import { getSignificantInsights, indicatorStatusForSignificantInsights, @@ -8,15 +8,15 @@ import { getGitHubMetaForCurrentApp } from '.'; export const getInsightsIndicatorStatus = async () => { const [ codeMeta, - photosCountOutdated, + photosCountNeedSync, ] = await Promise.all([ getGitHubMetaForCurrentApp(), - getOutdatedPhotosCount(), + getPhotosInNeedOfSyncCount(), ]); const significantInsights = getSignificantInsights({ codeMeta, - photosCountOutdated, + photosCountNeedSync, }); return indicatorStatusForSignificantInsights(significantInsights); diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index e0743f16..5c6dd3ba 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -2,7 +2,6 @@ import { BiCopy } from 'react-icons/bi'; import LoaderButton from './primitives/LoaderButton'; import clsx from 'clsx/lite'; import { toastSuccess } from '@/toast'; -import Tooltip from './Tooltip'; import { ComponentProps } from 'react'; export default function CopyButton({ @@ -10,20 +9,18 @@ export default function CopyButton({ text, subtle, iconSize = 15, - tooltip, - tooltipColor, className, + ...props }: { label: string text?: string, subtle?: boolean iconSize?: number - tooltip?: string - tooltipColor?: ComponentProps['color'] className?: string -}) { - const button = +} & ComponentProps) { + return ( } className={clsx( subtle && 'text-gray-300 dark:text-gray-700', @@ -37,13 +34,6 @@ export default function CopyButton({ : undefined} styleAs="link" disabled={!text} - />; - - return ( - tooltip - ? - {button} - - : button + /> ); } diff --git a/src/components/primitives/LoaderButton.tsx b/src/components/primitives/LoaderButton.tsx index fa105096..d865c945 100644 --- a/src/components/primitives/LoaderButton.tsx +++ b/src/components/primitives/LoaderButton.tsx @@ -2,9 +2,29 @@ import Spinner, { SpinnerColor } from '@/components/Spinner'; import { clsx } from 'clsx/lite'; -import { ButtonHTMLAttributes, ReactNode } from 'react'; +import { ButtonHTMLAttributes, ComponentProps, ReactNode } from 'react'; +import Tooltip from '../Tooltip'; -export default function LoaderButton(props: { +export default function LoaderButton({ + children, + isLoading, + icon, + spinnerColor, + spinnerClassName, + styleAs = 'button', + hideTextOnMobile = true, + confirmText, + shouldPreventDefault, + primary, + hideFocusOutline, + type = 'button', + onClick, + disabled, + className, + tooltip, + tooltipColor, + ...rest +}: { isLoading?: boolean icon?: ReactNode spinnerColor?: SpinnerColor @@ -15,27 +35,10 @@ export default function LoaderButton(props: { shouldPreventDefault?: boolean primary?: boolean hideFocusOutline?: boolean + tooltip?: string + tooltipColor?: ComponentProps['color'] } & ButtonHTMLAttributes) { - const { - children, - isLoading, - icon, - spinnerColor, - spinnerClassName, - styleAs = 'button', - hideTextOnMobile = true, - confirmText, - shouldPreventDefault, - primary, - hideFocusOutline, - type = 'button', - onClick, - disabled, - className, - ...rest - } = props; - - return ( + const button = + ; + + return ( + tooltip + ? + {button} + + : button ); } diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 42681111..246befa3 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -579,44 +579,22 @@ export const getPhoto = async ( // Sync queries -const outdatedWhereClause = - `WHERE updated_at < $1 OR (updated_at < $2 AND make = $3)`; +const outdatedWhereClauses = [ + `updated_at < $1`, + `(updated_at < $2 AND make = $3)`, +]; -const outdatedValues = [ +const outdatedWhereValues = [ UPDATED_BEFORE_01.toISOString(), UPDATED_BEFORE_02.toISOString(), MAKE_FUJIFILM, ]; -export const getOutdatedPhotos = () => safelyQueryPhotos( - () => query(` - SELECT * FROM photos - ${outdatedWhereClause} - ORDER BY created_at DESC - LIMIT ${SYNC_QUERY_LIMIT} - `, - outdatedValues, - ) - .then(({ rows }) => rows.map(parsePhotoFromDb)), - 'getOutdatedPhotos', -); - -export const getOutdatedPhotosCount = () => safelyQueryPhotos( - () => query(` - SELECT COUNT(*) FROM photos - ${outdatedWhereClause} - `, - outdatedValues, - ) - .then(({ rows }) => parseInt(rows[0].count, 10)), - 'getOutdatedPhotosCount', -); - -const photosThatNeedAiTextWhereClause = ( +const needsAiTextWhereClauses = ( AI_TEXT_GENERATION_ENABLED && AI_TEXT_AUTO_GENERATED_FIELDS.length ) - ? 'WHERE ' + AI_TEXT_AUTO_GENERATED_FIELDS + ? AI_TEXT_AUTO_GENERATED_FIELDS .map(field => { switch (field) { case 'title': return `(title <> '') IS NOT TRUE`; @@ -624,29 +602,32 @@ const photosThatNeedAiTextWhereClause = ( case 'tags': return `array_length(tags, 1) = 0`; case 'semantic': return `(semantic_description <> '') IS NOT TRUE`; } - }).join(' OR ') - : undefined; + }) + : []; -export const getPhotosThatNeedAiText = () => safelyQueryPhotos( - async () => photosThatNeedAiTextWhereClause - ? query(` - SELECT * FROM photos - ${photosThatNeedAiTextWhereClause} - ORDER BY created_at DESC - LIMIT ${SYNC_QUERY_LIMIT} - `) - .then(({ rows }) => rows.map(parsePhotoFromDb)) - : [] as Photo[], - 'getPhotosThatNeedAiText', +const needsSyncWhereStatement = + `WHERE ${outdatedWhereClauses.concat(needsAiTextWhereClauses).join(' OR ')}`; + +export const getPhotosInNeedOfSync = () => safelyQueryPhotos( + () => query(` + SELECT * FROM photos + ${needsSyncWhereStatement} + ORDER BY created_at DESC + LIMIT ${SYNC_QUERY_LIMIT} + `, + outdatedWhereValues, + ) + .then(({ rows }) => rows.map(parsePhotoFromDb)), + 'getPhotosInNeedOfSync', ); -export const getPhotosThatNeedAiTextCount = () => safelyQueryPhotos( - async () => photosThatNeedAiTextWhereClause - ? query(` - SELECT COUNT(*) FROM photos - ${photosThatNeedAiTextWhereClause} - `) - .then(({ rows }) => parseInt(rows[0].count, 10)) - : 0, - 'getPhotosThatNeedAiTextCount', +export const getPhotosInNeedOfSyncCount = () => safelyQueryPhotos( + () => query(` + SELECT COUNT(*) FROM photos + ${needsSyncWhereStatement} + `, + outdatedWhereValues, + ) + .then(({ rows }) => parseInt(rows[0].count, 10)), + 'getPhotosInNeedOfSyncCount', ); diff --git a/src/photo/index.ts b/src/photo/index.ts index e03c97cd..fba70537 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -23,7 +23,7 @@ import { isBefore } from 'date-fns'; import type { Metadata } from 'next'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; -import { doesPhotoNeedSync } from './sync'; +import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync'; // INFINITE SCROLL: FEED export const INFINITE_SCROLL_FEED_INITIAL = @@ -97,7 +97,7 @@ export interface PhotoDb extends updatedAt: Date createdAt: Date takenAt: Date - tags: string[] + tags?: string[] } // Parsed db response @@ -109,15 +109,16 @@ export interface Photo extends Omit { exposureTimeFormatted?: string exposureCompensationFormatted?: string takenAtNaiveFormatted: string + tags: string[] recipeData?: FujifilmRecipe - needsSync?: boolean + syncStatus: PhotoSyncStatus } export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { const photoDb = camelcaseKeys( photoDbRaw as unknown as Record, ) as unknown as PhotoDb; - const photo: Photo ={ + return { ...photoDb, tags: photoDb.tags ?? [], focalLengthFormatted: @@ -140,9 +141,8 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { ? JSON.parse(photoDb.recipeData) : photoDb.recipeData : undefined, - }; - photo.needsSync = doesPhotoNeedSync(photo); - return photo; + syncStatus: generatePhotoSyncStatus(photoDb), + } as Photo; }; export const parseCachedPhotoDates = (photo: Photo) => ({ diff --git a/src/photo/sync.ts b/src/photo/sync.ts index 47c76d32..4fb6d14e 100644 --- a/src/photo/sync.ts +++ b/src/photo/sync.ts @@ -1,14 +1,19 @@ import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; -import { Photo } from '.'; +import { PhotoDb } from '.'; import { AI_TEXT_AUTO_GENERATED_FIELDS } from '@/app/config'; +export interface PhotoSyncStatus { + isOutdated: boolean; + isMissingAiText: boolean; +} + export const SYNC_QUERY_LIMIT = 1000; export const UPDATED_BEFORE_01 = new Date('2024-06-16'); // UTC 2025-02-24 05:30:00 export const UPDATED_BEFORE_02 = new Date(Date.UTC(2025, 1, 24, 5, 30, 0)); -const isPhotoOutdated = (photo: Photo) => +const isPhotoOutdated = (photo: PhotoDb) => photo.updatedAt < UPDATED_BEFORE_01 || ( photo.updatedAt < UPDATED_BEFORE_02 && photo.make === MAKE_FUJIFILM @@ -19,12 +24,13 @@ const doesPhotoNeedAiText = ({ caption, tags = [], semanticDescription, -}: Photo) => +}: PhotoDb) => (AI_TEXT_AUTO_GENERATED_FIELDS.includes('title') && !title) || (AI_TEXT_AUTO_GENERATED_FIELDS.includes('caption') && !caption) || (AI_TEXT_AUTO_GENERATED_FIELDS.includes('tags') && tags.length === 0) || (AI_TEXT_AUTO_GENERATED_FIELDS.includes('semantic') && !semanticDescription); -export const doesPhotoNeedSync = (photo: Photo) => - isPhotoOutdated(photo) || - doesPhotoNeedAiText(photo); +export const generatePhotoSyncStatus = (photo: PhotoDb): PhotoSyncStatus => ({ + isOutdated: isPhotoOutdated(photo), + isMissingAiText: doesPhotoNeedAiText(photo), +});