Skip to content

fix: Apply the dev-vs-main review findings#114

Merged
OffCrazyFreak merged 22 commits into
devfrom
fix/ai-dev-vs-main-review-2026-07
Jul 24, 2026
Merged

fix: Apply the dev-vs-main review findings#114
OffCrazyFreak merged 22 commits into
devfrom
fix/ai-dev-vs-main-review-2026-07

Conversation

@OffCrazyFreak

@OffCrazyFreak OffCrazyFreak commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Implements the findings selected from the 7-reviewer sweep of dev vs main (5 Claude Opus 5 high, Codex gpt-5.6-sol high, CodeRabbit; Cursor was out of usage). 40 findings were ranked; 31 fixed here, 6 filed as issues, 1 rejected, 2 informational.

One commit per finding early on, then per logical batch. No behaviour is changed beyond what each finding describes.

Area 1: security and supply chain

Finding Change
1 The springdoc ignore had lost its update-types filter, so it suppressed security updates too. SecurityConfig serves /api-docs/** and /swagger-ui/** with permitAll(), so an unauthenticated Swagger surface pinned at 2.2.0 would never get a security PR. Now bounded at versions: [">=2.3.0"].
3 sharp devDependency raised to ^0.35.3 so its declared floor matches the security floor its own override asserts.

Area 2: correctness, time and data

The highest-confidence finding in the review, reached independently by three reviewers from both ends.

UserDto.lastLoginAt / lastActiveAt were LocalDateTime, so Jackson emitted "2026-07-24T13:05:11.123" with no offset and the browser parsed it as local time. In Zagreb a just-signed-in user rendered as "prije 2 sata", and the WAU/MAU window shifted by each admin's UTC offset.

  • 6 must land first: last_active_at was stamped with LocalDateTime.now() (JVM zone) but compared against MAX(session.updated_at), which drizzle writes in UTC. A nowUtc() helper makes the stored values genuinely UTC.
  • 4 then changes the two DTO fields and AuthIdentity to Instant, so the wire format carries Z. The zod schemas stay z.string(), so parsing is unaffected.
  • 10 replaces the silent-null timestamp coercion with one that handles Timestamp, Instant, OffsetDateTime and LocalDateTime and logs anything else.
  • 5 the activity cards no longer render a confident 0 while the query is pending or failed.
  • 7 the last-login cell no longer throws. formatRelativeTime has no invalid-date guard and RangeErrors on NaN, and useGetAllUsers returns raw axios data with no zod parse.

toInstant converts a Timestamp via toLocalDateTime() first on purpose: the driver reads a zone-less column in the JVM zone, so calling toInstant() directly would re-apply that offset.

Area 3: LabeledSelect refactor regressions

The sort-select to labeled-select extraction changed behaviour at its call sites.

Finding Change
11 The product page's chain sort control read "Optimiziraj po:", borrowed from the shopping-list optimize control. Restored to "Sortiraj po:".
12 The extraction dropped justify-end, w-full sm:w-60 and size="sm", so all three selects lost right alignment and mobile full width and grew taller. Verified the classes now match main.
13 options accepted any string while onValueChange promised TValue, closed by an unchecked cast. Now generic, and each call site is typed against its own union.
14 The trigger had no accessible name, so screen readers announced only the current value.
32 ILabeledSelectOption moved to @/typings, removing a util-to-component import.

Area 4: sidebar

16 SidebarProductNav called useSearchParams purely for active state, so the Suspense boundary put a skeleton rather than the Popusti / Karta / Statistika / Novosti anchors into the static HTML, partly undoing c11ea04. A new useClientSearchParams keeps the group in the prerender, and only SidebarFilterMenu sits behind a boundary now.

Agreed trade: the ?discounted=true highlight resolves just after hydration. This supersedes findings 17 and 35, which had just fixed and deduplicated the whole-group skeleton; the h-8 correction carries into the new SidebarFilterMenuSkeleton.

Area 5: shopping lists

Finding Change
33 The "parse every chain's avg_price, drop NaN, take min/max" block existed three times with three different empty-case behaviours. One getChainAvgPriceRange helper now.
22 productsData took a new identity every render, invalidating four memos and two memo() components. Now built through useQueries' combine, the repo's first use of it.
23 completeStoresAnalysis was computed, threaded through two components and never destructured. Deleted, along with its helper and type. computeAbsolutePrices and chainsStockingEveryItem stay.
20, 21 Cheapest and most-expensive were icon-plus-colour only, and the price row colour-only, failing WCAG 1.4.1.
37 "Optimiziraj po:" governs the locative; three of four labels were in the wrong case.

Area 6: dependencies and docs

Finding 24 was rejected as written. The reviewer called the direct kysely dependency dead weight because nothing imports it. It is not: @better-auth/core@1.6.25 declares kysely as a non-optional peer and the root entry is what satisfies it, with drizzle-orm resolving its optional peer against the same copy. Removing it would not break the build, since auto-install-peers defaults to true, but the version would float within ^0.28.5 || ^0.29.0 on an auth-critical package. The real defect was the missing rationale, now recorded in AGENTS.md.

25 rewrites the docs/DEPLOYMENT.md row that claimed pnpm-workspace.yaml overrides "weren't honored" and told readers to use a direct dependency instead. That stale row is what made kysely look unexplained in the first place; five overrides are live and the lockfile proves they apply.

Rejected

Codex's only backend finding claimed last_active_at was added without a migration. application.properties:11 sets spring.jpa.hibernate.ddl-auto=update, so Hibernate adds the column itself.

Filed as issues instead

#115 (pooled price range colours the wrong chain), #116 (badge vs table item sets), #117 (last login vanishes on sign-out), #118 (CI permissions: block), #119 (future timestamps inflate WAU/MAU), #120 (locked items prefetch six routes).

#115 and #116 are pre-existing: the shopping-list reviewer widened past the branch diff, and only three files under shopping-lists/ actually changed between main and dev.

Verification

  • prettier --check and tsc --noEmit clean across the branch, ignoring the known PageProps / RouteContext typegen noise.
  • Backend is CI-only, per AGENTS.md. This PR was opened as a draft after batch 1 specifically so mvn -B verify compiled the Instant change and the DAO extraction early; it passed.

Not yet verified on dev.disscount.me: the timezone fix, the select layout at 375px, and the sidebar anchors in prerendered HTML.

Changes:
- Add `versions: [">=2.3.0"]` to the springdoc ignore entry
- Note in the comment why the bound exists

A bare `dependency-name` ignore suppresses Dependabot security updates as
well as version updates. SecurityConfig serves /api-docs/**, /v3/api-docs/**
and /swagger-ui/** with permitAll(), so an unauthenticated Swagger surface
frozen at springdoc 2.2.0 would never receive a security PR. Bounding the
ignore at >=2.3.0 keeps the Boot-4-only line blocked while letting 2.2.x
patches and security fixes through.

Notes:
- Review finding 1 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Inert until this reaches main; Dependabot reads the config from the
  default branch only.
Changes:
- Add a private nowUtc() helper to UserService and use it for all six timestamp writes
- Stamp createdAt via LocalDateTime.now(ZoneOffset.UTC) in User.onCreate

latestOf compares app_user.last_active_at against MAX(session.updated_at),
which drizzle always writes in UTC. Stamping with LocalDateTime.now() used the
JVM default zone, so the two operands only shared a clock base while the JVM
happened to run UTC. Off Docker (local dev on Europe/Zagreb) the stamped column
sat 1-2h ahead and always won the merge, and the same column held a different
clock base per environment.

Notes:
- Review finding 6 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Prerequisite for finding 4: serialising these as UTC on the wire is only
  correct once the stored values genuinely are UTC.
- Scoped to the user domain. Other domains still stamp via BaseEntity in the
  JVM zone, which is harmless today since nothing compares them to better-auth.
Changes:
- Change UserDto.lastLoginAt / lastActiveAt and both AuthIdentity components to Instant
- Convert the app_user column with toUtcInstant() in convertToUserDto
- Replace toLocalDateTime with toInstant, handling Timestamp, Instant, OffsetDateTime
  and LocalDateTime, and logging an unmapped type instead of returning null

Jackson serialized LocalDateTime without an offset, so the browser parsed
"2026-07-24T13:05:11.123" as local time. In Zagreb that made a just-signed-in
user render as "prije 2 sata", and shifted the dashboard's WAU/MAU window by
the viewer's UTC offset so window-edge users landed in the wrong bucket and
counts differed per admin. The wire format is now "...Z".

The zod schemas stay z.string(), so parsing is unaffected; only the admin
dashboard reads these two fields.

Notes:
- Review findings 4 and 10 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- toInstant converts a Timestamp via toLocalDateTime() first on purpose: the
  driver reads a zone-less column in the JVM zone, so calling toInstant()
  directly would re-apply that offset and shift the value again.
- Depends on the preceding UTC-stamping commit; converting with ZoneOffset.UTC
  is only correct because the column is now written in UTC.
Changes:
- Add a <lombok.version> property set to 1.18.46
- Drop the hard-coded version from the lombok dependency
- Reference ${lombok.version} from the annotation processor path

The version was hard-coded in two places, so the last bump had to edit both
and a future bump that missed one would compile against a different lombok
than it declares.

Notes:
- Review finding 28 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- The property name is the one spring-boot-dependencies uses, so declaring it
  overrides the parent-managed version. Simply deleting both version elements
  would have downgraded lombok to whatever Boot 3.1.0 pins, undoing 3c40b6a.
Changes:
- Add AuthIdentityDao holding findByUserIds, deleteById and the timestamp coercion
- Inject it into UserService and drop the direct EntityManager dependency
- Remove the now-unused Timestamp, HashMap, OffsetDateTime and persistence imports

Reading and deleting rows in better-auth's `user` / `session` tables is a
different concern from the app_user profile service: that schema is owned by
the frontend's auth library and has no JPA entities, so it needs native
queries. Keeping it inline left UserService at 316 lines with two raw SQL
statements sitting next to the profile logic.

Notes:
- Review finding 34 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- UserService is now 291 lines, still above the AGENTS.md 50-100 target. The
  remaining bulk is genuine profile logic and would need a separate split.
@netlify

netlify Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploy Preview for disscount failed. Why did it fail? →

Name Link
🔨 Latest commit 7977ff0
🔍 Latest deploy log https://app.netlify.com/projects/disscount/deploys/6a63de9a4eafce00083cc430

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@OffCrazyFreak, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ae895e92-4028-48ce-9359-fb699ce652c3

📥 Commits

Reviewing files that changed from the base of the PR and between 8031a5a and 7977ff0.

⛔ Files ignored due to path filters (1)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • AGENTS.md
  • backend/pom.xml
  • backend/src/main/java/disscount/user/dto/UserDto.java
  • backend/src/main/java/disscount/user/service/UserService.java
  • docs/AUTH.md
  • docs/DEPLOYMENT.md
  • docs/STATE-PERSISTENCE.md
  • frontend/package.json
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/store-card-header.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/store-card-price-row.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/store-card-price-stat.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/store-optimize-select.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-store-chain-analysis.ts
  • frontend/src/app/(user)/shopping-lists/[id]/typings/store-chain-types.ts
  • frontend/src/app/(user)/shopping-lists/[id]/utils/shopping-list-items-table-utils.ts
  • frontend/src/app/(user)/shopping-lists/[id]/utils/store-chain-completeness.ts
  • frontend/src/app/(user)/shopping-lists/[id]/utils/store-chain-extremes.ts
  • frontend/src/app/(user)/shopping-lists/utils/store-sorting-utils.ts
  • frontend/src/app/dashboard/components/admin-activity-card.tsx
  • frontend/src/app/dashboard/components/admin-user-row.tsx
  • frontend/src/app/dashboard/components/admin-users-stats.tsx
  • frontend/src/app/dashboard/utils/user-activity.ts
  • frontend/src/app/products/[id]/components/product-chain-sort-select.tsx
  • frontend/src/app/products/utils/product-price-utils.ts
  • frontend/src/app/products/utils/product-utils.ts
  • frontend/src/components/custom/common/labeled-select.tsx
  • frontend/src/components/custom/common/relative-time.tsx
  • frontend/src/components/custom/sidebar/app-sidebar.tsx
  • frontend/src/components/custom/sidebar/sidebar-filter-menu-skeleton.tsx
  • frontend/src/components/custom/sidebar/sidebar-product-nav-shell.tsx
  • frontend/src/components/custom/sidebar/sidebar-product-nav-skeleton.tsx
  • frontend/src/components/custom/sidebar/sidebar-product-nav.tsx
  • frontend/src/hooks/use-client-search-params.ts
  • frontend/src/lib/api/schemas/auth-user.ts
  • frontend/src/typings/labeled-select-option.ts
  • frontend/src/utils/date.ts
  • frontend/src/utils/strings.ts

Walkthrough

Changes

Backend identity and UTC timestamps

Layer / File(s) Summary
Timestamp contracts and UTC initialization
backend/src/main/java/disscount/user/domain/User.java, backend/src/main/java/disscount/user/dto/AuthIdentity.java, backend/src/main/java/disscount/user/dto/UserDto.java
Entity creation uses UTC, and authentication and user DTO timestamps use Instant.
Authentication identity DAO
backend/src/main/java/disscount/user/dao/AuthIdentityDao.java
Native queries read aggregated identity/session timestamps and delete Better-Auth users.
User service UTC and identity integration
backend/src/main/java/disscount/user/service/UserService.java
User activity timestamps use UTC, identity data comes from AuthIdentityDao, and administrator deletion delegates identity removal through the DAO.

Dependency configuration

Layer / File(s) Summary
Dependency version rules
.github/dependabot.yml, backend/pom.xml
Dependabot applies a springdoc version constraint, and Maven centralizes Lombok’s version across dependency and annotation processor configuration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UserService
  participant AuthIdentityDao
  participant EntityManager
  UserService->>AuthIdentityDao: findByUserIds(userIds)
  AuthIdentityDao->>EntityManager: execute native identity/session query
  EntityManager-->>AuthIdentityDao: identity rows and timestamps
  AuthIdentityDao-->>UserService: AuthIdentity map
  UserService->>AuthIdentityDao: deleteById(userId)
  AuthIdentityDao->>EntityManager: execute native user delete
Loading

Possibly related PRs

Poem

I’m a UTC bunny, hopping through time,
With Instant timestamps aligned just fine.
Auth rows now flow through a DAO door,
Lombok versions match as never before.
Springdoc watches the versions in line!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the PR as applying review findings across the branch.
Description check ✅ Passed The description is clearly related to the listed backend, dependency, and timestamp changes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Changes:
- Export MILLISECONDS_PER_DAY from utils/date.ts
- Import it in user-activity.ts instead of redeclaring it

AGENTS.md requires checking utils/ for an existing helper before writing a new
one. The constant already existed but was module-private, so the dashboard
util declared a second copy of the same value.

Notes:
- Review finding 31 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
…ending

Changes:
- Read isLoading and isError from useGetAllUsers in AdminUsersStats
- Pass both into AdminActivityCard and render a Skeleton or the error text
- Show the count only once the query has resolved successfully

countActiveUsers(undefined, ...) returns 0, so both cards asserted a confident
"0 tjedno aktivnih korisnika" while the request was still in flight or had
failed, sitting right next to the users table's own error message. Zero was
indistinguishable from unknown.

Notes:
- Review finding 5 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Flagged independently by all three engines, the joint-highest consensus in
  the review.
- Error copy matches the users table so the panel reads consistently.
Changes:
- Track isDefaultWindow and show the specific title plus abbreviation only then
- Fall back to the generic "Aktivni korisnici" for any other window

The heading was fixed while the window is user-selectable, so picking
"Zadnjih 90 dana" left "Tjedno aktivni korisnici (WAU)" sitting over a 90-day
number, and both cards could be set to the same window and show the identical
figure under two different labels.

Notes:
- Review finding 38 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
Changes:
- Add formatDateTime to utils/strings, reusing formatDate for the date half
- Add a RelativeTime component that guards an unparsable value and waits for mount
- Render the last-login cell through it

formatRelativeTime has no invalid-date guard: it falls through its DIVISIONS
loop on NaN and calls Intl.RelativeTimeFormat.format(NaN, "years"), which
throws a RangeError and takes down the client tree. useGetAllUsers returns raw
axios data with no zod parse, unlike the contact and digital-card services, so
an unparsable lastLoginAt really can reach the cell.

The same line also read Date.now() during the server render with no mount
guard, so the relative label could mismatch on hydration and then never
refresh. RelativeTime follows the last-synced-label precedent and uses
useMinuteTick, rendering the absolute timestamp until mounted.

Notes:
- Review findings 7 and 39 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- The hydration half was not in the review; it was found while fixing finding 7.
- The tooltip now carries the time too, which finding 39 asked for: formatDate
  emits only dd.mm.yyyy., so hovering "prije 5 minuta" revealed nothing.
Changes:
- Split the shared comment so each field describes itself
- Note that lastActiveAt is present on every UserDto, not just the admin list

The single "Admin list only" comment covered both fields but was true only for
lastLoginAt: convertToUserDto sets lastActiveAt, so GET and PATCH /api/users/me
return it too. The comment is the only contract doc for these fields, so it
would have sent the next consumer to the admin list for data /me already has.

Notes:
- Review finding 27 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
Changes:
- Point admin-activity-card and user-activity at @/lib/api/schemas/auth-user

The dashboard was split two-vs-two: the newer WAU/MAU files went through the
@/lib/api/types barrel while the user-table files imported the schema module
directly. Both resolve to the same type, but one feature folder using two
paths for one type is churn waiting to happen.

Notes:
- Review finding 40 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Only the dashboard is normalised here. The contact-side files also use the
  barrel, but they are outside this review's scope.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/pom.xml`:
- Around line 25-27: Update the Lombok dependency declaration and its annotation
processor configuration to both use the existing ${lombok.version} property,
ensuring dependency compilation and annotation processing share the same
version.

In `@backend/src/main/java/disscount/user/service/UserService.java`:
- Around line 283-290: Update the last_active_at migration flow around
UserService.toUtcInstant so pre-UTC rows are converted using the prior
deployment’s host zone before being interpreted as UTC. Apply this backfill or
cutover-compatible representation only to historical values, while preserving
UTC handling for values written after the migration and keeping active-user
calculations consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aa6109eb-5a46-4524-872e-6648c524872a

📥 Commits

Reviewing files that changed from the base of the PR and between cbb605b and 8031a5a.

📒 Files selected for processing (7)
  • .github/dependabot.yml
  • backend/pom.xml
  • backend/src/main/java/disscount/user/dao/AuthIdentityDao.java
  • backend/src/main/java/disscount/user/domain/User.java
  • backend/src/main/java/disscount/user/dto/AuthIdentity.java
  • backend/src/main/java/disscount/user/dto/UserDto.java
  • backend/src/main/java/disscount/user/service/UserService.java

Comment thread backend/pom.xml Outdated
Comment on lines +25 to +27
<!-- Overrides the version spring-boot-dependencies manages, and is the single
source for both the dependency and the annotation processor path below. -->
<lombok.version>1.18.46</lombok.version>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply ${lombok.version} to the Lombok dependency too.

The dependency declaration still has no <version>, so the new property only controls the annotation processor. Maven may therefore compile against Spring Boot’s managed Lombok version while processing annotations with 1.18.46, contradicting the “single source” configuration and risking version-specific build failures.

Proposed fix
 		<dependency>
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>
+			<version>${lombok.version}</version>
 			<scope>provided</scope>
 		</dependency>

Also applies to: 76-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/pom.xml` around lines 25 - 27, Update the Lombok dependency
declaration and its annotation processor configuration to both use the existing
${lombok.version} property, ensuring dependency compilation and annotation
processing share the same version.

Comment on lines +283 to +290
.lastActiveAt(toUtcInstant(user.getLastActiveAt()))
.build();
}

// The column is zone-less but written by nowUtc(), so UTC is the offset it was stamped with.
private Instant toUtcInstant(LocalDateTime value) {
return value == null ? null : value.toInstant(ZoneOffset.UTC);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Migrate pre-UTC last_active_at rows before assigning UTC.

Line 289 reinterprets every existing zone-less value as UTC, but prior releases wrote these values using the host default zone. On non-UTC deployments, historical activity instants—and therefore active-user counters—shift by that offset. Backfill existing rows using the prior deployment zone (or preserve a cutover-compatible representation) before this API change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/java/disscount/user/service/UserService.java` around lines
283 - 290, Update the last_active_at migration flow around
UserService.toUtcInstant so pre-UTC rows are converted using the prior
deployment’s host zone before being interpreted as UTC. Apply this backfill or
cutover-compatible representation only to historical values, while preserving
UTC handling for values written after the migration and keeping active-user
calculations consistent.

Changes:
- Move ILabeledSelectOption to @/typings and make it generic over TValue
- Type options as readonly ILabeledSelectOption<TValue>[]
- Resolve the picked option instead of casting the raw string back to TValue
- Type each call site's array against its own union, adding ActivityWindowDays

options accepted any string while onValueChange promised TValue, and the gap
was closed by an unchecked `next as TValue`. A typo like "totl" compiled
cleanly and arrived at setStoreOptimizeMode as a mode that silently never
matches anything.

Moving the type also removes the coupling that made a dashboard util import
from a component module; AGENTS.md puts shared UI types in @/typings.

Notes:
- Review findings 13 and 32 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Finding 13 was flagged by all three engines.
- The default type parameter keeps ILabeledSelectOption usable unparameterised.
Changes:
- Generate a useId for the label span
- Reference it from SelectTrigger via aria-labelledby

The label was only visually adjacent, so the Radix role="combobox" trigger took
its accessible name from the current value. A screen reader announced
"Trgovinama, combobox" with no indication of what the control does, at every
call site. On the dashboard it was worse: two sibling cards each announced only
their selected period, with nothing tying either to WAU or MAU.

Notes:
- Review finding 14 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Flagged by all three engines, the broadest consensus in the review.
- aria-labelledby rather than aria-label so the announced name stays in sync
  with the visible text.
Changes:
- Put justify-end back on the wrapper and accept a className override
- Restore the trigger's w-full sm:w-60 and size="sm"
- Drop the comment explaining the min-h workaround it no longer needs

Extracting sort-select into LabeledSelect silently lost three layout classes
that both original call sites had: the control stopped being right-aligned,
stopped filling the width on mobile, and grew from the small trigger to the
default height. That is a visible regression on the product detail page and
the shopping-list stores panel.

The activity cards opt out of the right alignment, since they sit under a
left-aligned number rather than above a list.

Notes:
- Review findings 12 and 36 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Only Codex caught this one.
- Verified the wrapper and trigger classes now match main, modulo the
  tailwind plugin's class ordering.
Changes:
- Restore the sort wording on the product detail chain control

The LabeledSelect extraction swapped in "Optimiziraj po:", borrowed from the
shopping-list optimize control, which is a different concept. This picks how
the chains carrying one product are ordered, and its options are still the
dative "Trgovinama / Cijeni / Udaljenosti" written for "Sortiraj po", so the
control mislabelled itself on every product detail page.

Notes:
- Review finding 11 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Keeps the trailing colon, which is now the convention for every label
  rendered through LabeledSelect.
Changes:
- Change the child skeletons from h-7 to h-8

The real child row is SidebarFilterMenu, whose collapsible trigger is a plain
SidebarMenuButton at the default h-8, not a SidebarMenuSubButton at h-7. Every
child row therefore jumped 4px when the Suspense boundary resolved, which is
exactly what the file's own "Mirrors the real row heights so nothing shifts"
comment promises it does not do.

Notes:
- Review finding 17 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
Changes:
- Add SidebarProductNavShell holding the group, label and menu wrapper
- Render both the real nav and the skeleton through it

The group label "Istraži" and the SidebarGroup / SidebarGroupContent /
SidebarMenu structure were copy-pasted between the two files, so the skeleton
would silently stop matching the real nav as soon as either changed shape.
The h-7 versus h-8 mismatch fixed in the previous commit is what that drift
looks like in practice.

Notes:
- Review finding 35 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
Changes:
- Add useClientSearchParams, a client-only query-string read
- Use it in SidebarProductNav so the group no longer bails out of prerendering
- Wrap only the filter menus in Suspense, with a new SidebarFilterMenuSkeleton
- Drop the whole-group boundary from AppSidebar and delete its skeleton

SidebarProductNav called useSearchParams purely for active-state and filter
params, so the Suspense boundary around it put a skeleton, rather than the
Popusti / Karta / Statistika / Novosti anchors, into the static HTML. Those
hrefs come from a constant and need no request data, so this partly undid the
crawlable-nav fix in c11ea04.

The anchors now render statically and only SidebarFilterMenu, which genuinely
needs the URL, sits behind a boundary.

Notes:
- Review findings 16, 17 and 35 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Agreed trade: the ?discounted=true highlight on Popusti now resolves just
  after hydration instead of in the first paint. Navigating from /products to
  /products?discounted=true keeps the same pathname, so the highlight there
  settles on the next navigation; every other entry point is correct.
- This supersedes the whole-group skeleton that findings 17 and 35 had just
  fixed and deduplicated. The h-8 correction from 17 carries over into
  SidebarFilterMenuSkeleton, which is the only skeleton left.
…e extremes

Changes:
- Add getChainAvgPriceRange to product-price-utils and call it from all three
  duplicates (store-chain-extremes, shopping-list-items-table-utils, store-sorting-utils)
- Memoise productsData through useQueries' combine option
- Delete findCompleteStoresAnalysis, extremeByAvgPrice, ICompleteStoresAnalysis
  and the prop threading behind them
- Give the store-card header arrows aria-hidden plus sr-only text
- Split the price row into StoreCardPriceStat and add an sr-only extreme label
- Put the optimize option labels in the locative

The "parse every chain's avg_price, drop NaN, take min/max" block existed three
times with three different empty-case behaviours, which is exactly how the badge
and cell rules drifted apart before.

productsData was rebuilt outside any memo, so it took a new identity every
render and invalidated the allChains, cheapestCountByChain and
storesWithHighestPriceItems memos plus the two memo() components downstream.

completeStoresAnalysis was computed, threaded through two components and never
destructured, implying a "best basket" marker that was not wired up.

Cheapest and most-expensive were signalled by icon and colour only, with no
text alternative, even though shopping-list-item-row already sets that
precedent; the price row was colour-only, which fails WCAG 1.4.1.

Notes:
- Review findings 20, 21, 22, 23, 33, 36, 37, 40 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- combine is the repo's first use of that option; it is memoised by TanStack.
- computeAbsolutePrices and chainsStockingEveryItem are still live and untouched.
- "Optimiziraj po:" governs the locative, so three of the four labels were in
  the wrong case.
Changes:
- Raise the sharp devDependency to ^0.35.3 via pnpm add
- Note in AGENTS.md that kysely is a direct dep only to pin better-auth's peer
- Rewrite the stale DEPLOYMENT.md row that said pnpm overrides are not honored

The sharp override declares everything below 0.35.0 unsafe and forces ^0.35.3,
but the direct devDependency still asked for ^0.35.2, which the selector does
not match. That copy sat outside the override's protection and landed on a safe
version only by luck of "latest".

Three reviewers independently read kysely as dead weight because nothing
imports it. It is not: @better-auth/core declares kysely as a NON-optional peer
and the root entry is what satisfies it, with drizzle-orm resolving its own
optional peer against the same copy. Removing it would not break the build,
since auto-install-peers defaults to true, but the version would float within
"^0.28.5 || ^0.29.0" on an auth-critical package.

The DEPLOYMENT.md gotcha was the source of that confusion: it told readers
overrides do not work and to use a direct dependency instead.

Notes:
- Review findings 3, 24 and 25 (reviews/REVIEW-2026-07-24-DEV-VS-MAIN.md)
- Finding 24 asked for kysely to be removed. Rejected after checking the peer
  declarations; the real defect was the missing rationale, which is now recorded.
Changes:
- Collapse the multi-line explanatory comments added across this branch
- Run prettier over three shopping-list files the batch gate missed

AGENTS.md asks for single-line comments only where genuinely needed, and says
a multi-line explanation usually means the code should be rewritten instead.
Several comments introduced earlier on this branch broke that.

Notes:
- Comment-only for the backend; no behaviour change.
…chParams

Changes:
- AUTH.md: add AuthIdentityDao to the key-files map and note UserService stamps UTC
- AUTH.md: add three gotchas covering the naive-timestamp stack, why the two
  dashboard fields are Instant, and that lastLoginAt is derived rather than stored
- STATE-PERSISTENCE.md: expand the useSearchParams gotcha to explain that the
  Suspense fallback is what gets prerendered, and point at useClientSearchParams
- STATE-PERSISTENCE.md: correct the stale next version in the library table

The docs described the code as it was before this branch. The timezone fix and
the sidebar prerender fix both turned on behaviour that was written down
nowhere, which is why each took a full review to surface.

Notes:
- Docs only, no code change.
@OffCrazyFreak
OffCrazyFreak merged commit 0053f37 into dev Jul 24, 2026
3 of 7 checks passed
@OffCrazyFreak
OffCrazyFreak deleted the fix/ai-dev-vs-main-review-2026-07 branch July 24, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant