Skip to content

Commit aacae04

Browse files
ViktorSvertokaliudmylasovetovsLesiaUKR
authored
Hot fix (#381)
* (SP: 3) [Backend] add Nova Poshta shipping foundation + checkout persistence + async label workflow (#364) * (SP: 2) [Frontend] Reduce Vercel variable costs via caching and analytics cleanup (#367) * perf(vercel): cut runtime costs via notification, blog cache, and analytics changes * perf(blog): remove server searchParams usage to preserve ISR * fix(build): align Netlify Node version and remove SpeedInsights import * chore(release): bump version to 1.0.4 * (SP: 2) [Frontend] Remove [locale] layout force-dynamic and move auth to client-side (#370) * refactor(frontend): remove locale layout dynamic auth and move header auth client-side * fix(frontend): prevent stale auth responses in useAuth and remove redundant dashboard dynamic layout * (SP: 2) [Frontend] Reduce auth overhead and sync auth state across tabs (#372) * refactor(frontend): remove locale layout dynamic auth and move header auth client-side * fix(frontend): prevent stale auth responses in useAuth and remove redundant dashboard dynamic layout * feat(frontend): sync auth state across tabs via BroadcastChannel * (SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub stars cache (#371) * perf(quiz-flow): move quiz progress to client-side fetch, enable ISR for quizzes page - Move user progress fetch from SSR to client-side API (/api/quiz/progress) - Remove force-dynamic and getCurrentUser() from quizzes page - Add revalidate=300 for ISR caching - Use window.history.replaceState for tab URL sync (avoid Next.js navigation) - Add forceMount to TabsContent to prevent layout shift on tab switch - Fix nested <main> — use <section> inside DynamicGridBackground - Cache GitHub stars count in sessionStorage to avoid refetch + re-animation * perf: replace useRef with useState lazy initializer in GitHubStarButton Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot be read during render. Uses useState(getStoredStars) to capture the sessionStorage value once on mount instead. * fix: stop star icon trembling on hover in GitHubStarButton * (SP: 1) [Frontend] Fix quiz timer flash and card layout shift on quizzes page (#373) * perf(quiz-flow): move quiz progress to client-side fetch, enable ISR for quizzes page - Move user progress fetch from SSR to client-side API (/api/quiz/progress) - Remove force-dynamic and getCurrentUser() from quizzes page - Add revalidate=300 for ISR caching - Use window.history.replaceState for tab URL sync (avoid Next.js navigation) - Add forceMount to TabsContent to prevent layout shift on tab switch - Fix nested <main> — use <section> inside DynamicGridBackground - Cache GitHub stars count in sessionStorage to avoid refetch + re-animation * perf: replace useRef with useState lazy initializer in GitHubStarButton Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot be read during render. Uses useState(getStoredStars) to capture the sessionStorage value once on mount instead. * fix: stop star icon trembling on hover in GitHubStarButton * fix: eliminate quiz timer flash on language switch Remove Suspense boundary (loading.tsx) that unmounted QuizContainer during locale navigation. Synchronous session restore via useReducer lazy initializer and correct timer initialization via useState lazy initializer prevent any visible state reset on language switch * fix: replace quiz card layout shift with skeleton grid during progress load * chore(release): v1.0.5 * (SP: 3)[Shop][DB] Reduce Neon compute: throttle janitor + relax checkout polling + add sweep indexes (#375) * (SP: 3) [Backend] add internal janitor (jobs 1-4), claim/lease + runbook (G0-G6) * (SP: 3) [Backend] add provider selector, fix payments gating, i18n checkout errors * Add shop category images to public * (SP: 3) [Shop][Monobank] I1 structured logging: codes + logging safety checks * (SP: 3) [Shop][Monobank] Fail-closed non-browser origin posture for webhook + janitor (ORIGIN_BLOCKED) * (SP: 3) [Shop][Monobank] [Shop][Monobank] J gate: add orders status ownership test and pass all pre-prod invariants * (SP: 3) [Shop][Monobank] review fixes (tests, logging, success UI) * (SP: 1) [Shop][Monobank] Tighten webhook log-code typing; harden DB tests; minor security/log/UI cleanups * (SP: 1) [Shop][Monobank] harden Monobank webhook (origin/PII-safe logs) and remove duplicate sha256 hashing * (SP: 1) [Cart] adding route for user orders to cart page * (SP: 1) [Cart] fix after review cart mpage and adding index for orders * (SP: 1) [Cart] Fix cart orders summary auth rendering and return totalCount for orders badge * (SP: 1) [Cart] remove console.warn from CartPageClient to satisfy monobank logging safety invariant, namespace localStorage cart by user and reset on auth change * (SP: 1) [Cart] rehydrate per cartOwnerId (remove didHydrate coupling) * (SP: 2)[Backend] shop/shipping schema migrations foundation * (SP: 2)[Backend] shop/shipping public routes + np cache + sync * (SP: 2)[Backend] shop/shipping: shipping persistence + currency policy * (SP: 2)[Backend] shop/shipping: webhook apply + psp fields + enqueue shipping * (SP: 2)[Backend] shop/shipping: shipments worker + internal run + np mock * (SP: 2)[Backend] shop/shipping: admin+ui shipping actions * (SP: 2)[Backend] shop/shipping: retention + log sanitizer + metrics * (SP: 1)[Backend] stabilize Monobank janitor (job1/job3) and fix failing apply-outcomes tests * (SP: 1) [db]: add shop shipping core migration * (SP: 1) [FIX] resolve merge artifacts in order details page * (SP: 1) [FIX] apply post-review fixes for shipping and admin flows * (SP: 1) [FIX] align cart shipping imports (localeToCountry + availability reason code) * (SP: 1) [FIX] hard-block checkout when shipping disabled + i18n reason mapping * (SP: 1) [FIX] harden webhook enqueue + shipping worker + NP catalog + cart fail-closed * (SP: 1) [FIX] Initialize shippingMethodsLoading to true to avoid premature checkout. * (SP: 1) [FIX] migration 17 * (SP: 1) [DB] migrarion to testind DB and adjusting tests * (SP: 1)[DB] slow down restock janitor + enforce prod interval floor * (SP: 1) [DB] add order status lite view (opt-in) + instrumentation * (SP: 1) [DB] replace checkout success router.refresh polling with backoff API polling * (SP: 1) [DB] throttle sessions activity heartbeat + use count(*) (PK invariant) * (SP: 1)[DB] enforce production min intervals for internal shipping jobs * (SP: 1) [DB] add minimal partial indexes for orders sweeps + rollout notes * (SP: 1) [DB] refactor sweep claim step to FOR UPDATE SKIP LOCKED batching * (SP: 1)[DB]: slow janitor schedule to every 30 minutes * (SP: 1)[DB] increase polling delays for MonobankRedirectStatus * (SP: 1)[FIX] harden webhooks + fix SSR hydration + janitor/np gates + sweeps refactor * (SP: 1)[FIX] harden shipping enqueue gating + apply NP interval floor * (SP: 3) [SHOP] audit-driven e2e purchase readiness hardening (events, notifications, consent, returns) (#378) * (SP:3)[SHOP] add canonical payment/shipping/admin audit tables + dedupe helper with flagged atomic dual-write * (SP: 2)[SHOP] add INTL quote flow (request/offer/accept/decline), payment-init gate, and quote expiry/timeout sweeps * (SP: 3)[SHOP] introduce outbox-driven notifications with projector + worker (phase 3) * (SP: 3)[SHOP] add minimal returns/RMA lifecycle with atomic audit + canonical events (phase 4) * (SP: 3)[SHOP] enforce guest status-token lite-only access and audit token usage (phase 5) * (SP: 3)[SHOP] centralize transition guards and enforce across admin/webhook/worker flows (phase 6) :wq n * (SP:3)[SHOP]: enforce DATABASE_URL_LOCAL preflight + deterministic vitest config * (SP:3)[SHOP]: require canonical events in prod (fail-fast) * (SP:3)[SHOP]: implement notifications transport with retries + DLQ * (SP:3)[SHOP]: persist checkout legal consent artifact * (SP:1)[SHOP] add migration 0025 for consent + events + audit + prices * (SP:1)[SHOP] emit shipping_events for shipment worker transitions * (SP:2)[SHOP] audit admin product mutations (deduped) * (SP:2)[SHOP] make Monobank webhook retryable on transient apply failures * (SP:3) [SHOP] enforce guest status-token scopes across order actions * (SP:1) [SHOP] make product_prices the only write authority * (SP:1) [SHOP] add Playwright e2e smoke suite (local DB only) * (SP:3) [SHOP] explicitly reject exchanges (EXCHANGES_NOT_SUPPORTED) * (SP: 3)[FIX] harden audit + workers/tests; fix transitions/restock + webhook perf * (SP: 3)[FIX] harden local-db test safety and tighten shop reliability guards * (SP: 3)[FIX] fail-closed admin audit, refine shipments-worker outcomes/metrics, tighten quote+tests * (SP: 1)[FIX] harden shipments-worker claiming/leases and make audit+quote/test paths resilient * (SP: 1)[FIX] harden quote request errors/logging and sanitize requestId; document best-effort delete audit * feat(ui): add devops/cloud category icons and styles (#379) * chore(release): prepare v1.0.6 changelog * chore: bump version to 1.0.6 * fix(orders): close missing brace in checkout shipping snapshot try block * fix(checkout): correct nested try/catch structure for shipping snapshot * fix(order-status): remove stale responseMode lite branch --------- Co-authored-by: Liudmyla Sovetovs <milkaegik@gmail.com> Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.com>
1 parent e0d1e01 commit aacae04

101 files changed

Lines changed: 2423 additions & 4109 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/actions/notifications.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use server';
22

3-
import { and,desc, eq } from 'drizzle-orm';
3+
import { and, desc, eq } from 'drizzle-orm';
44

55
import { db } from '@/db';
66
import { notifications } from '@/db/schema/notifications';
@@ -9,7 +9,7 @@ import { getCurrentUser } from '@/lib/auth';
99
export async function getNotifications() {
1010
const session = await getCurrentUser();
1111
if (!session) return [];
12-
12+
1313
try {
1414
const data = await db.query.notifications.findMany({
1515
where: eq(notifications.userId, session.id),
@@ -53,7 +53,12 @@ export async function markAllAsRead() {
5353
await db
5454
.update(notifications)
5555
.set({ isRead: true })
56-
.where(and(eq(notifications.userId, session.id), eq(notifications.isRead, false)));
56+
.where(
57+
and(
58+
eq(notifications.userId, session.id),
59+
eq(notifications.isRead, false)
60+
)
61+
);
5762

5863
return { success: true };
5964
} catch (error) {

frontend/actions/profile.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function updateName(formData: FormData) {
2222

2323
try {
2424
await updateUser(session.id, { name: name.trim() });
25-
25+
2626
// Create notification
2727
const tNotify = await getTranslations('notifications.account');
2828
await createNotification({
@@ -62,7 +62,7 @@ export async function updatePassword(formData: FormData) {
6262
const { db } = await import('@/db');
6363
const { users } = await import('@/db/schema/users');
6464
const { eq } = await import('drizzle-orm');
65-
65+
6666
const dbUser = await db.query.users.findFirst({
6767
where: eq(users.id, session.id),
6868
});

frontend/app/[locale]/admin/shop/orders/[id]/ShippingActions.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ function actionEnabled(args: {
1919
}): boolean {
2020
if (args.action === 'retry_label_creation') {
2121
return (
22-
args.shipmentStatus === 'failed' || args.shipmentStatus === 'needs_attention'
22+
args.shipmentStatus === 'failed' ||
23+
args.shipmentStatus === 'needs_attention'
2324
);
2425
}
2526
if (args.action === 'mark_shipped') {

frontend/app/[locale]/dashboard/page.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export default async function DashboardPage({
165165
const outlineBtnStyles =
166166
'inline-flex items-center justify-center rounded-full border border-gray-200/50 bg-white/10 px-6 py-2.5 text-sm font-semibold tracking-wide text-gray-700 backdrop-blur-md transition-all hover:-translate-y-0.5 hover:bg-white/20 hover:shadow-md hover:border-gray-300 dark:border-white/10 dark:bg-neutral-900/40 dark:text-gray-200 dark:hover:bg-neutral-800/80 dark:hover:border-white/20';
167167

168-
const sponsorBtnStyles =
168+
const sponsorBtnStyles =
169169
'group relative inline-flex items-center justify-center gap-2 rounded-full border border-(--accent-primary)/30 bg-(--accent-primary)/10 px-6 py-2.5 text-sm font-semibold tracking-wide text-(--accent-primary) backdrop-blur-md transition-all hover:-translate-y-0.5 hover:bg-(--accent-primary)/20 hover:shadow-[0_4px_12px_rgba(var(--accent-primary-rgb),0.2)] hover:border-(--accent-primary)/50 dark:border-(--accent-primary)/20 dark:bg-(--accent-primary)/5 dark:hover:bg-(--accent-primary)/20 dark:hover:border-(--accent-primary)/40 dark:hover:shadow-[0_4px_15px_rgba(var(--accent-primary-rgb),0.3)] overflow-hidden';
170170

171171
return (
@@ -188,7 +188,7 @@ export default async function DashboardPage({
188188
href="#feedback"
189189
className={`group flex items-center gap-2 ${outlineBtnStyles}`}
190190
>
191-
<MessageSquare className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white" />
191+
<MessageSquare className="h-4 w-4 text-gray-400 transition-transform group-hover:-translate-y-0.5 group-hover:text-gray-600 dark:group-hover:text-white" />
192192
{t('supportLink')}
193193
</a>
194194
<a
@@ -199,10 +199,12 @@ export default async function DashboardPage({
199199
>
200200
{/* Subtle gradient glow background effect */}
201201
<div className="absolute inset-0 z-0 bg-linear-to-r from-transparent via-(--accent-primary)/10 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
202-
202+
203203
<span className="relative z-10 flex items-center gap-2">
204204
<Heart className="h-4 w-4 transition-transform group-hover:scale-110 group-hover:fill-(--accent-primary)/20" />
205-
{isMatchedSponsor ? t('profile.supportAgain') : t('profile.becomeSponsor')}
205+
{isMatchedSponsor
206+
? t('profile.supportAgain')
207+
: t('profile.becomeSponsor')}
206208
</span>
207209
</a>
208210
</div>
@@ -216,9 +218,13 @@ export default async function DashboardPage({
216218
totalAttempts={totalAttempts}
217219
globalRank={globalRank}
218220
/>
219-
<div id="stats" className="grid gap-8 scroll-mt-8 lg:grid-cols-2">
221+
<div id="stats" className="grid scroll-mt-8 gap-8 lg:grid-cols-2">
220222
<StatsCard stats={stats} attempts={lastAttempts} />
221-
<ActivityHeatmapCard attempts={attempts} locale={locale} currentStreak={currentStreak} />
223+
<ActivityHeatmapCard
224+
attempts={attempts}
225+
locale={locale}
226+
currentStreak={currentStreak}
227+
/>
222228
</div>
223229
</div>
224230
<div className="mt-8">

frontend/app/[locale]/quizzes/page.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { getTranslations } from 'next-intl/server';
33

44
import QuizzesSection from '@/components/quiz/QuizzesSection';
55
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
6-
import {
7-
getActiveQuizzes,
8-
} from '@/db/queries/quizzes/quiz';
6+
import { getActiveQuizzes } from '@/db/queries/quizzes/quiz';
97

108
type PageProps = { params: Promise<{ locale: string }> };
119

@@ -21,7 +19,7 @@ export async function generateMetadata({
2119
};
2220
}
2321

24-
export const revalidate = 300
22+
export const revalidate = 300;
2523

2624
export default async function QuizzesPage({ params }: PageProps) {
2725
const { locale } = await params;

frontend/app/api/quiz/progress/route.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ export async function GET() {
99
const user = await getCurrentUser();
1010

1111
if (!user?.id) {
12-
return NextResponse.json({}, {
13-
headers: { 'Cache-Control': 'no-store' },
14-
});
12+
return NextResponse.json(
13+
{},
14+
{
15+
headers: { 'Cache-Control': 'no-store' },
16+
}
17+
);
1518
}
1619

1720
const rawProgress = await getUserQuizzesProgress(user.id);
18-
const progressMap: Record<string, { bestScore: number; totalQuestions: number; attemptsCount: number }> = {};
21+
const progressMap: Record<
22+
string,
23+
{ bestScore: number; totalQuestions: number; attemptsCount: number }
24+
> = {};
1925

2026
for (const [quizId, progress] of rawProgress) {
2127
progressMap[quizId] = {

frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ import {
1111
import { logError, logWarn } from '@/lib/logging';
1212
import { requireAdminCsrf } from '@/lib/security/admin-csrf';
1313
import { guardBrowserSameOrigin } from '@/lib/security/origin';
14-
import {
15-
InvalidPayloadError,
16-
OrderNotFoundError,
17-
} from '@/lib/services/errors';
14+
import { InvalidPayloadError, OrderNotFoundError } from '@/lib/services/errors';
1815
import { offerIntlQuote } from '@/lib/services/shop/quotes';
1916
import {
2017
intlQuoteOfferPayloadSchema,
@@ -133,7 +130,10 @@ export async function POST(
133130
);
134131
}
135132
if (error instanceof AdminForbiddenError) {
136-
return noStoreJson({ code: error.code, message: 'Forbidden.' }, { status: 403 });
133+
return noStoreJson(
134+
{ code: error.code, message: 'Forbidden.' },
135+
{ status: 403 }
136+
);
137137
}
138138
if (error instanceof OrderNotFoundError) {
139139
return noStoreJson({ code: error.code }, { status: 404 });

frontend/app/api/shop/admin/orders/[id]/shipping/route.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,17 @@ export async function POST(
144144
}
145145

146146
if (error instanceof AdminUnauthorizedError) {
147-
return noStoreJson({ code: error.code, message: 'Unauthorized.' }, { status: 401 });
147+
return noStoreJson(
148+
{ code: error.code, message: 'Unauthorized.' },
149+
{ status: 401 }
150+
);
148151
}
149152

150153
if (error instanceof AdminForbiddenError) {
151-
return noStoreJson({ code: error.code, message: 'Forbidden.' }, { status: 403 });
154+
return noStoreJson(
155+
{ code: error.code, message: 'Forbidden.' },
156+
{ status: 403 }
157+
);
152158
}
153159

154160
if (error instanceof ShippingAdminActionError) {

frontend/app/api/shop/internal/shipping/shipments/run/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { NextRequest, NextResponse } from 'next/server';
55

66
import { db } from '@/db';
77
import { requireInternalJanitorAuth } from '@/lib/auth/internal-janitor';
8-
import { getNovaPoshtaConfig, getShopShippingFlags, NovaPoshtaConfigError } from '@/lib/env/nova-poshta';
8+
import {
9+
getNovaPoshtaConfig,
10+
getShopShippingFlags,
11+
NovaPoshtaConfigError,
12+
} from '@/lib/env/nova-poshta';
913
import { logError, logInfo, logWarn } from '@/lib/logging';
1014
import { guardNonBrowserFailClosed } from '@/lib/security/origin';
1115
import {
@@ -87,7 +91,8 @@ async function readJsonBodyOrDefault(request: NextRequest): Promise<unknown> {
8791
}
8892

8993
export async function POST(request: NextRequest) {
90-
const requestId = request.headers.get('x-request-id')?.trim() || crypto.randomUUID();
94+
const requestId =
95+
request.headers.get('x-request-id')?.trim() || crypto.randomUUID();
9196
const runId = crypto.randomUUID();
9297
const baseMeta = {
9398
requestId,

frontend/app/api/shop/orders/[id]/quote/accept/route.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import { NextRequest } from 'next/server';
44

55
import { logError, logWarn } from '@/lib/logging';
66
import { guardBrowserSameOrigin } from '@/lib/security/origin';
7-
import {
8-
InvalidPayloadError,
9-
OrderNotFoundError,
10-
} from '@/lib/services/errors';
7+
import { InvalidPayloadError, OrderNotFoundError } from '@/lib/services/errors';
118
import { authorizeOrderMutationAccess } from '@/lib/services/shop/order-access';
129
import { acceptIntlQuote } from '@/lib/services/shop/quotes';
1310
import {

0 commit comments

Comments
 (0)