Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@
"date_range": "Date range",
"day": "Day",
"days": "Days",
"days_ago": "{days, plural, one {# day} other {# days}} ago",
"deduplicate_all": "Deduplicate All",
"deduplication_criteria_1": "Image size in bytes",
"deduplication_criteria_2": "Count of EXIF data",
Expand Down Expand Up @@ -1233,6 +1234,7 @@
"large_files": "Large Files",
"last": "Last",
"last_seen": "Last seen",
"last_upload": "Last uploaded ",
"latest_version": "Latest Version",
"latitude": "Latitude",
"leave": "Leave",
Expand Down Expand Up @@ -1398,6 +1400,7 @@
"networking_settings": "Networking",
"networking_subtitle": "Manage the server endpoint settings",
"never": "Never",
"never_uploaded": "Never uploaded",
"new_album": "New Album",
"new_api_key": "New API Key",
"new_date_range": "New date range",
Expand Down Expand Up @@ -2037,6 +2040,7 @@
"to_parent": "Go to parent",
"to_select": "to select",
"to_trash": "Trash",
"today": "Today",
"toggle_settings": "Toggle settings",
"total": "Total",
"total_usage": "Total usage",
Expand Down
14 changes: 13 additions & 1 deletion mobile/openapi/lib/model/user_admin_response_dto.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions open-api/immich-openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -17866,6 +17866,11 @@
"isAdmin": {
"type": "boolean"
},
"lastAssetUploadedAt": {
"format": "date-time",
"nullable": true,
"type": "string"
},
"license": {
"allOf": [
{
Expand Down Expand Up @@ -17923,6 +17928,7 @@
"email",
"id",
"isAdmin",
"lastAssetUploadedAt",
"license",
"name",
"oauthId",
Expand Down
1 change: 1 addition & 0 deletions open-api/typescript-sdk/src/fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export type UserAdminResponseDto = {
email: string;
id: string;
isAdmin: boolean;
lastAssetUploadedAt: string | null;
license: (UserLicense) | null;
name: string;
oauthId: string;
Expand Down
3 changes: 3 additions & 0 deletions server/src/dtos/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export class UserAdminResponseDto extends UserResponseDto {
@ValidateEnum({ enum: UserStatus, name: 'UserStatus' })
status!: string;
license!: UserLicense | null;
@ApiProperty({ type: 'string', format: 'date-time', nullable: true })
lastAssetUploadedAt!: Date | null;
}

export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
Expand All @@ -187,5 +189,6 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,
license: license ? { ...license, activatedAt: new Date(license?.activatedAt) } : null,
lastAssetUploadedAt: null,
};
}
13 changes: 13 additions & 0 deletions server/src/queries/asset.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ set
where
"assetId" in ($2)

-- AssetRepository.getLatestCreatedAtForUser
select
"createdAt"
from
"asset"
where
"ownerId" = $1::uuid
and "deletedAt" is null
order by
"createdAt" desc
limit
$2

-- AssetRepository.updateDateTimeOriginal
update "asset_exif"
set
Expand Down
13 changes: 13 additions & 0 deletions server/src/repositories/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ export class AssetRepository {
await this.db.updateTable('asset_exif').set(options).where('assetId', 'in', ids).execute();
}

@GenerateSql({ params: [DummyValue.UUID] })
getLatestCreatedAtForUser(ownerId: string) {
return this.db
.selectFrom('asset')
.select('createdAt')
.where('ownerId', '=', asUuid(ownerId))
.where('deletedAt', 'is', null)
.orderBy('createdAt', 'desc')
.limit(1)
.executeTakeFirst()
.then((row) => row?.createdAt ?? null);
}

@GenerateSql({ params: [[DummyValue.UUID], DummyValue.NUMBER, DummyValue.STRING] })
@Chunked()
async updateDateTimeOriginal(
Expand Down
7 changes: 6 additions & 1 deletion server/src/services/user-admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export class UserAdminService extends BaseService {

async get(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
const user = await this.findOrFail(id, { withDeleted: true });
return mapUserAdmin(user);
const dto = mapUserAdmin(user);

const last = await this.assetRepository.getLatestCreatedAtForUser(id);
dto.lastAssetUploadedAt = last ?? null;

return dto;
}

async update(auth: AuthDto, id: string, dto: UserAdminUpdateDto): Promise<UserAdminResponseDto> {
Expand Down
1 change: 1 addition & 0 deletions server/test/repositories/asset.repository.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
upsertMetadata: vitest.fn(),
getMetadataByKey: vitest.fn(),
deleteMetadataByKey: vitest.fn(),
getLatestCreatedAtForUser: vitest.fn().mockResolvedValue(null),
};
};
22 changes: 22 additions & 0 deletions web/src/routes/admin/users/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
mdiChartPie,
mdiChartPieOutline,
mdiCheckCircle,
mdiCloudUpload,
mdiDeleteRestore,
mdiFeatureSearchOutline,
mdiLockSmart,
Expand All @@ -47,6 +48,7 @@
mdiPlayCircle,
mdiTrashCanOutline,
} from '@mdi/js';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
Expand All @@ -69,6 +71,21 @@
let canResetPassword = $derived($authUser.id !== user.id);
let newPassword = $state<string>('');
let lastUploadText = $derived(
Copy link
Collaborator Author

@aviv926 aviv926 Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also use this option which is even more accurate in terms of time (even to the minute level) and it also uses the local language for the hour and time.
Let me know what you think

Suggested change
let lastUploadText = $derived(
let lastUploadText = $derived(
user.lastAssetUploadedAt
? (DateTime.fromISO(user.lastAssetUploadedAt).toRelative({ locale: $locale }) ?? $t('unknown_time'))
: $t('never_uploaded'),
);

user.lastAssetUploadedAt
? (() => {
const dt = DateTime.fromJSDate(new Date(user.lastAssetUploadedAt));
const now = DateTime.now();
if (dt.hasSame(now, 'day')) {
return $t('today');
} else {
const days = Math.floor(now.diff(dt, 'days').days);
return $t('days_ago', { values: { days } });
}
})()
: $t('never_uploaded'),
);
let editedLocale = $derived(findLocale($locale).code);
let createAtDate: Date = $derived(new Date(user.createdAt));
let updatedAtDate: Date = $derived(new Date(user.updatedAt));
Expand Down Expand Up @@ -345,6 +362,11 @@
</div>
</div>
{/if}
<div class="px-4 pb-4 flex items-center gap-1">
<Icon icon={mdiCloudUpload} size="1.25rem" class="text-primary" />
<Heading tag="h3" size="tiny">{$t('last_upload')}</Heading>
<Text>{lastUploadText}</Text>
</div>
</CardBody>
</Card>
<Card color="secondary">
Expand Down
4 changes: 2 additions & 2 deletions web/src/routes/admin/users/[id]/+page.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { AppRoute } from '$lib/constants';
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
import { getUserAdmin, getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true });
await requestServerInfo();
const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []);
const user = await getUserAdmin({ id: params.id }).catch(() => undefined);
if (!user) {
redirect(302, AppRoute.ADMIN_USERS);
}
Expand Down
1 change: 1 addition & 0 deletions web/src/test-data/factories/user-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({
activationKey: 'activation-key',
activatedAt: new Date().toISOString(),
},
lastAssetUploadedAt: Sync.each(() => faker.date.recent().toISOString()),
profileChangedAt: Sync.each(() => faker.date.recent().toISOString()),
});
Loading