feat(timeline): add pulse animation for in-flight drag mutations

Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:28:46 +02:00
parent 7a5e98e2e9
commit 1df208dbcc
386 changed files with 657 additions and 81650 deletions
+23 -1
View File
@@ -62,6 +62,10 @@ type StoredAliasMap = Record<string, StoredAliasEntry>;
const ALIAS_NAME_RE = /^[A-Za-z]+(?: [A-Za-z]+)*$/;
const ALIAS_SLUG_RE = /^[a-z]+(?:\.[a-z]+)*$/;
// Module-level TTL cache for the anonymization directory (60 s)
let _directoryCache: { value: AnonymizationDirectory | null; expiresAt: number } | null = null;
const DIRECTORY_TTL_MS = 60_000;
const ICONIC_ALIAS_NAMES = [
"Iron Man",
"Spider Man",
@@ -650,6 +654,10 @@ export async function getAnonymizationConfig(
};
}
export function invalidateAnonymizationDirectoryCache() {
_directoryCache = null;
}
export async function getAnonymizationDirectory(
db: Pick<PrismaClient, "systemSettings" | "resource">,
): Promise<AnonymizationDirectory | null> {
@@ -657,6 +665,11 @@ export async function getAnonymizationDirectory(
return null;
}
const now = Date.now();
if (_directoryCache && _directoryCache.expiresAt > now) {
return _directoryCache.value;
}
const settings = await db.systemSettings.findUnique({
where: { id: "singleton" },
select: {
@@ -736,13 +749,22 @@ export async function getAnonymizationDirectory(
where: { id: "singleton" },
data: { anonymizationAliases: storedAliases },
});
// Invalidate stale cache after a DB write so the next call re-fetches
_directoryCache = null;
}
return {
const directory: AnonymizationDirectory = {
config,
byResourceId,
byAliasEid,
};
// Only cache stable directories (no alias changes = steady state)
if (!aliasesChanged) {
_directoryCache = { value: directory, expiresAt: Date.now() + DIRECTORY_TTL_MS };
}
return directory;
}
export function anonymizeResource<T extends ResourceIdentity>(
+2 -15
View File
@@ -1,4 +1,5 @@
import { getPublicHolidays, type AbsenceDay } from "@capakraken/shared";
import { getPublicHolidays, toIsoDate, normalizeCityName, normalizeStateCode, type AbsenceDay } from "@capakraken/shared";
export { toIsoDate } from "@capakraken/shared";
type VacationLike = {
startDate: Date;
@@ -69,10 +70,6 @@ export function asHolidayResolverDb(db: unknown): HolidayResolverDb {
return db as HolidayResolverDb;
}
export function toIsoDate(value: Date): string {
return value.toISOString().slice(0, 10);
}
type CityHolidayRule = {
countryCode: string;
cityName: string;
@@ -93,16 +90,6 @@ const SCOPE_WEIGHT: Record<CalendarScope, number> = {
CITY: 3,
};
function normalizeCityName(cityName?: string | null): string | null {
const normalized = cityName?.trim().toLowerCase();
return normalized && normalized.length > 0 ? normalized : null;
}
function normalizeStateCode(stateCode?: string | null): string | null {
const normalized = stateCode?.trim().toUpperCase();
return normalized && normalized.length > 0 ? normalized : null;
}
function resolveCalendarEntries(
calendars: HolidayCalendarRecord[],
periodStart: Date,
+2 -1
View File
@@ -1,3 +1,4 @@
import { MILLISECONDS_PER_DAY } from "@capakraken/shared";
import { getCalendarHolidayStrings, toIsoDate } from "./holiday-availability.js";
type VacationSpan = {
@@ -59,7 +60,7 @@ export function countCalendarDaysInPeriod(
}
const ms = overlap.end.getTime() - overlap.start.getTime();
return Math.round(ms / 86_400_000) + 1;
return Math.round(ms / MILLISECONDS_PER_DAY) + 1;
}
export function countVacationChargeableDays(