Skip to content

feat: flow item view#153

Merged
hmbanan666 merged 2 commits into
mainfrom
flow-views
Sep 15, 2025
Merged

feat: flow item view#153
hmbanan666 merged 2 commits into
mainfrom
flow-views

Conversation

@hmbanan666

@hmbanan666 hmbanan666 commented Sep 15, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Flow items now track views: pages auto-mark as viewed and show viewer avatars.
    • Home navigation shows a badge with unviewed items count.
    • New “Create” button added to the main page navigation.
  • UI/Style

    • Refined heading sizes across multiple pages and cards for consistency.
    • Item cards show a “Not viewed” indicator until opened.
    • Navigation badges only show when non-zero and use an alert color.
  • Chores

    • Updated dependencies (Nuxt, nuxt-auth-utils) and added a Rollup resolution for improved stability.

@hmbanan666 hmbanan666 self-assigned this Sep 15, 2025
@coderabbitai

coderabbitai Bot commented Sep 15, 2025

Copy link
Copy Markdown

Walkthrough

Adds a full-stack “item views” feature for Flow: database tables/types, repository method, API endpoint, store methods/computed, page logic with auto-mark-as-viewed and avatars, and navigation badge showing unviewed count. Also updates navigation header with a conditional button, tweaks badge logic, and applies multiple presentational typography/heading changes. Minor dependency bumps.

Changes

Cohort / File(s) Summary of changes
Flow views feature (full stack)
apps/atrium-telegram/app/pages/flow/[itemId]/index.vue, apps/atrium-telegram/app/stores/flow.ts, apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts, packages/database/src/tables.ts, packages/database/src/repository/flow.ts, packages/database/src/types.ts, apps/atrium-telegram/shared/types/index.ts, apps/atrium-telegram/app/components/flow/ItemCard.vue, apps/atrium-telegram/app/stores/user.ts
Introduces view tracking: DB table and relations for flow item views; repository loads and creates views; API POST to record a view; flow store adds addView() and nowViewedItemsCount; Flow page auto-marks as viewed and shows viewer avatars; ItemCard shows “unviewed” indicator; user store exposes getAvatarUrl; shared types include views.
Navigation and badges
apps/atrium-telegram/app/components/Navigation.vue, apps/atrium-telegram/app/components/NavigationButton.vue, apps/atrium-telegram/app/composables/useNavigation.ts
Navigation layout height increase and new conditional header button on main page; home route gains dynamic badge from flowStore.nowViewedItemsCount; navigation chip hides when badge is empty or '0' and uses error color; composable exposes isMainPage.
Typography and heading adjustments
apps/atrium-telegram/app/components/EpicCard.vue, apps/atrium-telegram/app/components/flow/FeedbackAverage.vue, apps/atrium-telegram/app/components/flow/KitchensOnline.vue, apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue, apps/atrium-telegram/app/components/flow/OrdersOnline.vue, apps/atrium-telegram/app/pages/epic/[epicId]/index.vue, apps/atrium-telegram/app/pages/tasks/index.vue
Font size/weight and heading level updates; line clamp and muted color tweaks in EpicCard; no logic changes in these files.
Dependency and workspace updates
package.json, pnpm-workspace.yaml
Adds resolution for rollup ^4.50.2; bumps catalog versions: nuxt ^4.1.2, nuxt-auth-utils ^0.5.24.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant P as Flow Item Page<br/>([itemId]/index.vue)
  participant FS as Flow Store
  participant API as POST /api/flow/id/:itemId/view
  participant DB as Repository/DB

  U->>P: Open item page
  P->>P: Compute isViewed (by userId in item.views)
  alt Not viewed
    P->>FS: addView(itemId)
    FS->>API: POST view { userId, itemId } (auth)
    API->>DB: createItemView
    DB-->>API: View created
    API-->>FS: { ok: true }
    FS->>FS: update() refresh items
    FS-->>P: items with updated views
    note over P: Avatars render from item.views
  else Already viewed
    P-->>U: Render existing avatars
  end
Loading
sequenceDiagram
  participant Nav as Navigation (UI)
  participant Comp as useNavigation()
  participant FS as Flow Store

  FS-->>Comp: nowViewedItemsCount (computed)
  Comp-->>Nav: routes with home.badge = count.toString()
  Nav-->>Nav: Show chip when badge && badge !== '0'
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit peeked at flows today,
Left tiny tracks where views now stay.
A badge now blinks, “Go see! Go see!” 🥕
Avatars wink in jubilee.
Headers trimmed, the styles align—
Hop, click, mark viewed—then all is fine.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: flow item view" is concise, follows conventional commit style, and accurately captures the primary change in the changeset — adding view-tracking for flow items across the database, repository, API, store, and UI — so it is clear and relevant for teammates scanning history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch flow-views

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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 and usage tips.

@sonarqubecloud

Copy link
Copy Markdown

@hmbanan666 hmbanan666 merged commit d2bc6b2 into main Sep 15, 2025
7 of 8 checks passed
@hmbanan666 hmbanan666 deleted the flow-views branch September 15, 2025 09:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (12)
apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue (1)

7-12: Prefer locale-aware currency formatting (Intl) and simplify markup

Render the value with Intl.NumberFormat and drop the separate ₽ element.

-          <h3 class="text-2xl/5 font-bold">
-            {{ averageToday }}
-          </h3>
-          <p class="text-lg/4">
-
-          </p>
+          <h3 class="text-2xl/5 font-bold">
+            {{ formatCurrency(averageToday) }}
+          </h3>

Add alongside your script setup:

const formatCurrency = (n: number) =>
  new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(n)
apps/atrium-telegram/app/components/EpicCard.vue (1)

5-11: LGTM with a tiny UX note

The new title/description styles look good. If you notice odd wrapping, consider revisiting the combo of whitespace-pre-wrap with line-clamp.

packages/database/src/repository/flow.ts (1)

10-12: Trim related data or compute per-user flag server-side to avoid payload bloat.

Eager-loading full views for every item in both findItem and listItems can get large. If the UI only needs to know whether the current user has viewed an item, prefer:

  • returning a boolean (e.g., isViewedByMe) via an EXISTS subquery, or
  • limiting relation columns to just id and userId (if your Drizzle version supports relation column selection).

This reduces memory and network costs on the hot path.

Also applies to: 24-25

apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)

5-12: Parameter validation message clarity.

Minor: say itemId instead of generic Id for consistency with the route.

-        message: 'Id is required',
+        message: 'itemId is required',
apps/atrium-telegram/app/components/flow/ItemCard.vue (1)

3-15: Nice UX touch — clear “not viewed” indicator.

The animated pointer + label effectively signals attention needed. Keep it lightweight; consider aria-live="polite" on the container for SR users.

-      <div v-if="!isViewed" class="flex flex-row items-center gap-1.5 text-error">
+      <div v-if="!isViewed" class="flex flex-row items-center gap-1.5 text-error" aria-live="polite">
apps/atrium-telegram/app/stores/flow.ts (1)

34-54: Treat duplicate view POSTs as success; prefer optimistic update over full refetch.

  • Swallow 400/409 from POST /api/flow/id/:id/view and avoid refetching the whole list when possible; optionally perform an optimistic local update to the item's views.

Minimal change to swallow duplicates (apps/atrium-telegram/app/stores/flow.ts):

  async function addView(itemId: string) {
    try {
      await $fetch(`/api/flow/id/${itemId}/view`, {
        method: 'POST',
        headers: {
          Authorization: `tma ${initDataRaw.value}`,
        },
      })

      await update()
    } catch (error) {
      if (error instanceof Error) {
        if (error.message.includes('401')) {
          // No
        }
        if (error.message.includes('404')) {
          // Not found
        }
+        if (error.message.includes('400') || error.message.includes('409')) {
+          // Already viewed — treat as success
+          await update()
+          return
+        }
      }
    }
  }

Optional improvement: instead of calling update(), append the current user to the item's views locally (matching existing checks against userStore.id in flow pages/components) to avoid the full refetch.

apps/atrium-telegram/app/stores/user.ts (1)

114-117: Reuse existing finder; avoid duplicate scans and keep API cohesive.

Call the existing find() helper instead of rescanning users here.

Apply:

-  function getAvatarUrl(userId: string): string | undefined {
-    const user = users.value.find((user) => user.id === userId)
-    return user?.avatarUrl ?? undefined
-  }
+  function getAvatarUrl(userId: string): string | undefined {
+    return find(userId)?.avatarUrl ?? undefined
+  }
apps/atrium-telegram/app/components/Navigation.vue (1)

2-17: Plus button has no action; wire a handler or hide it; add ARIA.

Currently it’s inert and not accessible.

Apply:

-      <UButton
+      <UButton
         v-if="isMainPage"
         variant="solid"
         color="secondary"
         size="xl"
         class="transition-all duration-200 ease-in-out motion-preset-slide-down motion-duration-200"
         icon="i-lucide-plus"
+        aria-label="Create flow item"
+        @click="emit('create-flow-item')"
         :ui="{
           base: 'size-12 font-bold rounded-full',
           leadingIcon: 'size-6 mx-auto',
         }"
       />

And in script:

-const { isNavigationShown, mainRoutes, isMainPage } = useNavigation()
+const { isNavigationShown, mainRoutes, isMainPage } = useNavigation()
+const emit = defineEmits<{
+  (e: 'create-flow-item'): void
+}>()
apps/atrium-telegram/app/components/NavigationButton.vue (1)

25-26: Guard badge visibility with a numeric check.

Prevents showing badges like "NaN"/"undefined" if a bad value slips through.

Apply:

-        color="error"
-        :show="!!route.badge && route.badge !== '0'"
+        color="error"
+        :show="Number(route.badge) > 0"
apps/atrium-telegram/app/composables/useNavigation.ts (1)

16-16: Coerce safely; consider clamping large counts.

String(...) avoids .toString() on a non-number by accident and you may want to cap at e.g. 99+.

Apply:

-      badge: flowStore.nowViewedItemsCount.toString(),
+      badge: String(flowStore.nowViewedItemsCount),
+      // optionally: badge: String(Math.min(flowStore.nowViewedItemsCount, 99)),
apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)

17-29: Avoid O(n×m) lookups; precompute an avatar map.

getAvatarUrl() does a linear search per view. Precompute a lookup once for smoother lists.

Apply:

-        <UAvatar
-          v-for="view in item?.views"
-          :key="view.id"
-          :src="userStore.getAvatarUrl(view.userId)"
-        />
+        <UAvatar
+          v-for="view in item?.views"
+          :key="view.id"
+          :src="avatarUrlByUserId.get(view.userId)"
+        />

And in script:

 const userStore = useUserStore()
 const flowStore = useFlowStore()
 const item = computed(() => flowStore.items.find((item) => item.id === params.itemId))
+const avatarUrlByUserId = computed(() => {
+  const map = new Map<string, string | undefined>()
+  for (const u of userStore.users) map.set(u.id, u.avatarUrl ?? undefined)
+  return map
+})

45-51: View‑on‑open logic: add a small guard to avoid double POSTs.

A rapid route change could fire multiple writes before refresh. Gate with a local flag.

Apply:

-const isViewed = computed(() => item.value?.views.some((view) => view.userId === userStore?.id))
+const isViewed = computed(() => Boolean(item.value?.views.some((view) => view.userId === userStore.id)))

+let posting = false
 watch(isViewed, () => {
-  if (!isViewed.value && item.value?.id) {
-    flowStore.addView(item.value.id)
+  if (!isViewed.value && item.value?.id && !posting) {
+    posting = true
+    flowStore.addView(item.value.id).finally(() => { posting = false })
   }
 }, { immediate: true })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 612b235 and d1d0f58.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • apps/atrium-telegram/app/components/EpicCard.vue (1 hunks)
  • apps/atrium-telegram/app/components/Navigation.vue (2 hunks)
  • apps/atrium-telegram/app/components/NavigationButton.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/FeedbackAverage.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/ItemCard.vue (2 hunks)
  • apps/atrium-telegram/app/components/flow/KitchensOnline.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/OrdersOnline.vue (1 hunks)
  • apps/atrium-telegram/app/composables/useNavigation.ts (2 hunks)
  • apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1 hunks)
  • apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2 hunks)
  • apps/atrium-telegram/app/pages/tasks/index.vue (1 hunks)
  • apps/atrium-telegram/app/stores/flow.ts (2 hunks)
  • apps/atrium-telegram/app/stores/user.ts (2 hunks)
  • apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1 hunks)
  • apps/atrium-telegram/shared/types/index.ts (2 hunks)
  • packages/database/src/repository/flow.ts (3 hunks)
  • packages/database/src/tables.ts (3 hunks)
  • packages/database/src/types.ts (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/atrium-telegram/app/stores/flow.ts (1)
apps/atrium-telegram/app/stores/user.ts (1)
  • useUserStore (8-141)
apps/atrium-telegram/shared/types/index.ts (1)
packages/database/src/types.ts (1)
  • FlowItemView (209-209)
apps/atrium-telegram/app/composables/useNavigation.ts (1)
apps/atrium-telegram/app/stores/flow.ts (1)
  • useFlowStore (3-64)
apps/atrium-telegram/app/stores/user.ts (1)
packages/database/src/tables.ts (1)
  • users (73-91)
packages/database/src/repository/flow.ts (2)
packages/database/src/types.ts (1)
  • FlowItemViewDraft (210-210)
packages/database/src/tables.ts (1)
  • flowItemViews (821-833)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (18)
apps/atrium-telegram/app/components/flow/KitchensOnline.vue (1)

13-15: Typography tweak LGTM

The size change aligns with the rest of the flow components.

apps/atrium-telegram/app/pages/tasks/index.vue (1)

11-13: Heading hierarchy improvement LGTM

Switching to h1 on a top-level page is appropriate.

apps/atrium-telegram/app/components/flow/FeedbackAverage.vue (1)

8-12: LGTM

Consistent with the typography pass.

apps/atrium-telegram/app/components/flow/OrdersOnline.vue (1)

6-8: LGTM

Typography aligns with the other flow widgets.

apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)

16-19: h1 switch LGTM

Matches page semantics for a primary title.

pnpm-workspace.yaml (1)

57-58: Nuxt & nuxt-auth-utils patch bumps — verify workspace refs & Node engine

  • Repo shows "catalog:" workspace refs for nuxt/nuxt-auth-utils in: apps/atrium-telegram/package.json, apps/geo-vault/package.json, apps/storefront-telegram/package.json, apps/web-app/package.json, apps/web-storefront/package.json, packages/ui/package.json — semver ranges not extractable from these refs; verify resolved versions (pnpm -w list or lockfile).
  • engines.node is pinned to ">=22.18.0" (package "roll-stack"); verify Node >=22 is intentional and compatible with Nuxt ^4.1.2 and nuxt-auth-utils ^0.5.24.
packages/database/src/types.ts (1)

209-210: LGTM! Type definitions follow established patterns.

The FlowItemView and FlowItemViewDraft type definitions are correctly implemented using InferSelectModel and InferInsertModel, consistent with other entity types in the file.

apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)

35-39: Prefer treating duplicates as success (idempotent semantics).

With onConflictDoNothing, return { ok: true } regardless of prior view state (HTTP 200/204). This simplifies clients and avoids leaking state via errors.

If you keep duplicate detection, ensure it’s covered by tests; otherwise, remove and rely on DB uniqueness. Consider adding an integration test for double-POST behavior.

apps/atrium-telegram/app/components/flow/ItemCard.vue (1)

46-52: Fix non-reactive prop destructure in ItemCard.vue — keep userStore.id as-is

File: apps/atrium-telegram/app/components/flow/ItemCard.vue (lines ~46–52)

  • Destructuring defineProps breaks reactivity — make the prop a ref (toRef) so parent updates re-render.
  • userStore.id is provided by the Pinia setup store and is proxied/auto-unwrapped, so comparing userStore.id to a string is fine (no storeToRefs required).

Suggested change:

-import { format } from 'date-fns'
+import { format } from 'date-fns'
+import { toRef } from 'vue'
 import { ru } from 'date-fns/locale/ru'
 
-const { item } = defineProps<{
-  item: FlowItemWithData
-}>()
-
-const userStore = useUserStore()
-const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id))
+const props = defineProps<{ item: FlowItemWithData }>()
+const item = toRef(props, 'item')
+
+const userStore = useUserStore()
+const isViewed = computed(() => item.value.views.some((view) => view.userId === userStore.id))

Likely an incorrect or invalid review comment.

apps/atrium-telegram/app/stores/user.ts (1)

139-139: LGTM: exposing getAvatarUrl in the store API.

Public surface makes sense for UI call sites.

packages/database/src/tables.ts (2)

1312-1313: LGTM: added back‑relation for views on flowItems.


1326-1335: LGTM: relations for flowItemViewsuser and item.

apps/atrium-telegram/shared/types/index.ts (2)

1-1: LGTM: type import for FlowItemView.


13-14: LGTM, but ensure API always returns views: [].

Schema looks good; please confirm server responses always include a concrete array to avoid null/undefined at call sites.

apps/atrium-telegram/app/components/Navigation.vue (1)

34-34: LGTM: isMainPage usage matches the new header behavior.

apps/atrium-telegram/app/composables/useNavigation.ts (1)

7-7: LGTM: bringing flowStore into navigation.

apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)

8-11: LGTM: heading tweak.


41-41: LGTM: store usage consistent.

Comment on lines +6 to +7
const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: comparing a string to a Ref — count will be wrong for all users.

useUserStore().id is a Ref. Comparing view.userId === useUserStore().id always fails, so every item looks “not viewed”.

Fix by hoisting the store and unwrapping id. Consider renaming the var (typo) to notViewedItemsCount.

-  const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)
+  const userStore = useUserStore()
+  const { id: userId } = storeToRefs(userStore)
+  const notViewedItemsCount = computed(() =>
+    items.value.filter((item) => !item.views.some((view) => view.userId === userId.value)).length
+  )

Also export the renamed symbol below.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)
const userStore = useUserStore()
const { id: userId } = storeToRefs(userStore)
const notViewedItemsCount = computed(() =>
items.value.filter((item) => !item.views.some((view) => view.userId === userId.value)).length
)
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/flow.ts around lines 6-7, the computed uses
useUserStore().id (a Ref) inline so the equality always fails; hoist the store
outside the computed (const userStore = useUserStore()), compare view.userId to
userStore.id.value (unwrap the Ref), rename the variable from
nowViewedItemsCount to notViewedItemsCount, and update the export(s) below to
export the new name.

Comment on lines +59 to 63
nowViewedItemsCount,

update,
addView,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Export the renamed counter.

If you adopt notViewedItemsCount, update exports:

-    nowViewedItemsCount,
+    notViewedItemsCount,

Grepping for usages to update call sites:


🏁 Script executed:

#!/bin/bash
rg -n "nowViewedItemsCount|notViewedItemsCount" apps/atrium-telegram

Length of output: 366


Export renamed counter and update usages

If you rename nowViewedItemsCount → notViewedItemsCount, update the export in apps/atrium-telegram/app/stores/flow.ts (def: line 6, export: line 59) and replace all references — e.g. apps/atrium-telegram/app/composables/useNavigation.ts:16 (flowStore.nowViewedItemsCount → flowStore.notViewedItemsCount).

🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/flow.ts around lines 59-63 the store is still
exporting nowViewedItemsCount but the variable was renamed to
notViewedItemsCount; update the export to export notViewedItemsCount (replace
nowViewedItemsCount) and then update all usages across the repo to the new name
(for example change flowStore.nowViewedItemsCount to
flowStore.notViewedItemsCount in
apps/atrium-telegram/app/composables/useNavigation.ts line ~16 and any other
references).

Comment on lines +13 to +29
// Guards:
// If not exist
// If already viewed
const item = await repository.flow.findItem(itemId)
if (!item) {
throw createError({
statusCode: 404,
message: 'Item not found',
})
}
if (item.views.some((view) => view.userId === event.context.user.id)) {
throw createError({
statusCode: 400,
message: 'Already viewed',
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid TOCTOU and remove heavy duplicate-check query.

You fetch the whole item (including views) to detect duplicates, but this is race-prone and heavier than necessary. With a DB unique constraint and idempotent insert:

  • Keep the existence check for the item.
  • Drop the “already viewed” scan; let the insert no-op on conflict and return success.
-    // Guards:
-    // If not exist
-    // If already viewed
     const item = await repository.flow.findItem(itemId)
     if (!item) {
       throw createError({ statusCode: 404, message: 'Item not found' })
     }
-    if (item.views.some((view) => view.userId === event.context.user.id)) {
-      throw createError({ statusCode: 400, message: 'Already viewed' })
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Guards:
// If not exist
// If already viewed
const item = await repository.flow.findItem(itemId)
if (!item) {
throw createError({
statusCode: 404,
message: 'Item not found',
})
}
if (item.views.some((view) => view.userId === event.context.user.id)) {
throw createError({
statusCode: 400,
message: 'Already viewed',
})
}
const item = await repository.flow.findItem(itemId)
if (!item) {
throw createError({
statusCode: 404,
message: 'Item not found',
})
}
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts around lines 13
to 29, remove the in-memory duplicate check that scans item.views for the
current user; keep the existence check for the item but do not load the full
views collection. Instead perform an idempotent DB insert for the view (INSERT
... ON CONFLICT DO NOTHING / upsert) so duplicate attempts become no-ops and are
safe from TOCTOU races; if using an ORM, use its upsert/insert-ignore API or
catch unique-constraint violations and return success, ensuring the handler
always returns success when the view already exists.

Comment on lines +30 to +34
await repository.flow.createItemView({
itemId,
userId: event.context.user.id,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add explicit auth guard before accessing event.context.user.id.

If event.context.user is absent, this will throw. Return 401 early.

+    if (!event.context.user?.id) {
+      throw createError({ statusCode: 401, message: 'Unauthorized' })
+    }
     await repository.flow.createItemView({
       itemId,
       userId: event.context.user.id,
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await repository.flow.createItemView({
itemId,
userId: event.context.user.id,
})
if (!event.context.user?.id) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
await repository.flow.createItemView({
itemId,
userId: event.context.user.id,
})
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts around lines 30
to 34, the handler accesses event.context.user.id without guarding against a
missing user; add an explicit auth guard that checks event.context.user (or
event.context.user?.id) before calling repository.flow.createItemView and if
absent immediately return a 401 response (or throw an unauthorized HTTP error)
so the function exits early and never attempts to read .id on undefined.

Comment on lines +34 to +37
static async createItemView(data: FlowItemViewDraft) {
const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
return view
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Eliminate duplicate views with DB-level uniqueness and idempotent insert.

Current duplicate-prevention happens in app code and is race-prone (TOCTOU). Enforce a unique (itemId, userId) constraint and make the insert idempotent.

Apply this diff here to guard the insert:

   static async createItemView(data: FlowItemViewDraft) {
-    const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
+    const [view] = await useDatabase()
+      .insert(flowItemViews)
+      .values(data)
+      .onConflictDoNothing({ target: [flowItemViews.itemId, flowItemViews.userId] })
+      .returning()
     return view
   }

And add a unique constraint in packages/database/src/tables.ts (outside this diff):

 import { flowItems } from './tables' // existing imports for context
+import { uniqueIndex } from 'drizzle-orm/pg-core'
 
 export const flowItemViews = pgTable('flow_item_views', {
   // ...existing columns...
   userId: cuid2('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
   itemId: cuid2('item_id').notNull().references(() => flowItems.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
-})
+}, (t) => ({
+  uniq_item_user: uniqueIndex('flow_item_views_item_id_user_id_uidx').on(t.itemId, t.userId),
+}))

Run to confirm the unique index doesn’t already exist and to estimate current dupes:


🏁 Script executed:

#!/bin/bash
# Check schema for an existing unique index
rg -n "flow_item_views.*unique|uniqueIndex|unique\\(" -C2 -- packages/database/src

# (Optional) If you have DB access in your env, count duplicates (replace with your psql conn):
# psql -c "SELECT item_id, user_id, COUNT(*) FROM flow_item_views GROUP BY 1,2 HAVING COUNT(*)>1 ORDER BY COUNT(*) DESC;"

Length of output: 1948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) repo-wide occurrences of 'flow_item_views' or 'flowItemViews':"
rg -n --hidden --no-ignore "flow_item_views|flowItemViews" || true

echo
echo "2) If packages/database/src/tables.ts contains the table, show 20 lines around the first match:"
match_line=$(rg -n --hidden --no-ignore "flow_item_views" packages/database/src/tables.ts -S | head -n1 || true)
if [ -n "$match_line" ]; then
  file=$(echo "$match_line" | cut -d: -f1)
  line=$(echo "$match_line" | cut -d: -f2)
  start=$((line-12)); if [ "$start" -lt 1 ]; then start=1; fi
  sed -n "${start},$((line+60))p" "$file"
else
  echo "No direct 'flow_item_views' match in packages/database/src/tables.ts"
  rg -n --hidden --no-ignore "pgTable\\(|export const .* = pgTable" packages/database/src/tables.ts -n -S || true
  sed -n '1,300p' packages/database/src/tables.ts || true
fi

echo
echo "3) show createItemView in packages/database/src/repository/flow.ts (around the function):"
rg -n --hidden --no-ignore "createItemView|flowItemViews|flow_item_views" packages/database/src/repository/flow.ts -n -S || true
sed -n '1,240p' packages/database/src/repository/flow.ts || true

echo
echo "4) search for unique/index/onConflict usage anywhere relevant:"
rg -n --hidden --no-ignore "uniqueIndex\\(|unique\\(|index\\(|onConflictDoNothing|onConflictDoUpdate|onConflict\\(" -S || true

echo
echo "5) search migrations / sql files for 'flow_item_views':"
rg -n --hidden --no-ignore "flow_item_views" -S || true

Length of output: 1076


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== flowItemViews occurrences (files + lines) ==="
rg -n --hidden --no-ignore "flowItemViews|flow_item_views" || true

echo
echo "=== lines 760-920 of packages/database/src/tables.ts (table definition area) ==="
sed -n '760,920p' packages/database/src/tables.ts || true

echo
echo "=== Search for uniqueIndex or unique constraints in tables.ts ==="
rg -n --hidden --no-ignore "uniqueIndex|unique\\(|uniq_item_user|flow_item_views_item_id_user_id" packages/database/src/tables.ts || true

echo
echo "=== show createItemView function in packages/database/src/repository/flow.ts ==="
sed -n '1,200p' packages/database/src/repository/flow.ts || true

echo
echo "=== repo-wide search for onConflict usage ==="
rg -n --hidden --no-ignore "onConflictDoNothing|onConflictDoUpdate|onConflict\\(" || true

echo
echo "=== search migrations/sql for flow_item_views references ==="
rg -n --hidden --no-ignore "flow_item_views" || true

Length of output: 8578


Enforce DB uniqueness for flow_item_views and make createItemView idempotent.

No composite unique index exists and createItemView currently inserts without conflict handling — add a DB-level uniqueness on (itemId, userId) and use onConflictDoNothing in the insert.

Files to change:

  • packages/database/src/tables.ts (flowItemViews definition, ~line 821)
  • packages/database/src/repository/flow.ts (createItemView, ~line 35)

Apply these edits:

--- packages/database/src/repository/flow.ts
+++ packages/database/src/repository/flow.ts
@@
   static async createItemView(data: FlowItemViewDraft) {
-    const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
+    const [view] = await useDatabase()
+      .insert(flowItemViews)
+      .values(data)
+      .onConflictDoNothing({ target: [flowItemViews.itemId, flowItemViews.userId] })
+      .returning()
     return view
   }
+import { uniqueIndex } from 'drizzle-orm/pg-core'
 ...
 export const flowItemViews = pgTable('flow_item_views', {
   // ...existing columns...
   userId: cuid2('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
   itemId: cuid2('item_id').notNull().references(() => flowItems.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
-})
+}, (t) => ({
+  uniq_item_user: uniqueIndex('flow_item_views_item_id_user_id_uidx').on(t.itemId, t.userId),
+}))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static async createItemView(data: FlowItemViewDraft) {
const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
return view
}
static async createItemView(data: FlowItemViewDraft) {
const [view] = await useDatabase()
.insert(flowItemViews)
.values(data)
.onConflictDoNothing({ target: [flowItemViews.itemId, flowItemViews.userId] })
.returning()
return view
}
Suggested change
static async createItemView(data: FlowItemViewDraft) {
const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
return view
}
import { uniqueIndex } from 'drizzle-orm/pg-core'
...
export const flowItemViews = pgTable('flow_item_views', {
// ...existing columns...
userId: cuid2('user_id').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
}, (t) => ({
uniq_item_user: uniqueIndex('flow_item_views_item_id_user_id_uidx').on(t.itemId, t.userId),
}))
🤖 Prompt for AI Agents
In packages/database/src/tables.ts around line 821, add a composite unique
constraint/index on flow_item_views for (itemId, userId) to enforce DB-level
uniqueness; in packages/database/src/repository/flow.ts around line 35, make
createItemView idempotent by using insert(...).onConflictDoNothing() so insert
does not throw on duplicate, then if the insert returned no rows fetch and
return the existing row for that (itemId, userId) pair (i.e., attempt insert
with onConflictDoNothing and returning(), and if nothing is returned run a
select by itemId and userId and return that record).

Comment on lines +821 to +834
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Enforce uniqueness and add indexes for race‑safe, performant view tracking.

Without a DB‑level unique constraint, two concurrent inserts can create duplicate (itemId, userId) rows despite app‑level checks. Add a composite unique index and a read index on itemId.

Apply:

 export const flowItemViews = pgTable('flow_item_views', {
   id: cuid2('id').defaultRandom().primaryKey(),
   createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
   updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
   userId: cuid2('user_id').notNull().references(() => users.id, {
     onDelete: 'cascade',
     onUpdate: 'cascade',
   }),
   itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
     onDelete: 'cascade',
     onUpdate: 'cascade',
   }),
-})
+}, (t) => ({
+  byItem: index('flow_item_views_item_idx').on(t.itemId),
+  byItemUser: uniqueIndex('flow_item_views_item_user_uniq').on(t.itemId, t.userId),
+}))

And import the index helpers:

-import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
+import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core'

To verify there isn’t already a unique/index for this table:


🏁 Script executed:

#!/bin/bash
rg -nC2 'flow_item_views|uniqueIndex|index\(' packages/database/src/tables.ts

Length of output: 244


Enforce uniqueness and add indexes for race‑safe, performant view tracking.

Prevent duplicate (itemId, userId) rows on concurrent inserts — add a composite unique index and an index on itemId. Verified: no existing index/uniqueIndex for flow_item_views in packages/database/src/tables.ts.

Apply:

 export const flowItemViews = pgTable('flow_item_views', {
   id: cuid2('id').defaultRandom().primaryKey(),
   createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
   updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
   userId: cuid2('user_id').notNull().references(() => users.id, {
     onDelete: 'cascade',
     onUpdate: 'cascade',
   }),
   itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
     onDelete: 'cascade',
     onUpdate: 'cascade',
   }),
-})
+}, (t) => ({
+  byItem: index('flow_item_views_item_idx').on(t.itemId),
+  byItemUser: uniqueIndex('flow_item_views_item_user_uniq').on(t.itemId, t.userId),
+}))

And import the index helpers:

-import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
+import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
})
import { boolean, date, integer, jsonb, numeric, pgTable, text, timestamp, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core'
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
}, (t) => ({
byItem: index('flow_item_views_item_idx').on(t.itemId),
byItemUser: uniqueIndex('flow_item_views_item_user_uniq').on(t.itemId, t.userId),
}))
🤖 Prompt for AI Agents
In packages/database/src/tables.ts around lines 821 to 834, the flow_item_views
table needs a composite unique constraint on (itemId, userId) and a separate
index on itemId to prevent duplicate rows under concurrent inserts and to speed
lookups; import the index helper(s) (e.g., uniqueIndex and index) at the top of
the file and add
uniqueIndex('flow_item_views_item_user_unique').on(flowItemViews.itemId,
flowItemViews.userId) and
index('flow_item_views_item_idx').on(flowItemViews.itemId) (or equivalent helper
usage for your schema library) immediately after the table definition.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (15)
apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)

26-30: Always-true length check.

epic?.comments.length >= 0 is always true when comments exists. Use > 0 for intent.

-      <div v-if="epic?.comments && epic?.comments.length >= 0" class="flex flex-row items-center gap-2">
+      <div v-if="epic?.comments && epic?.comments.length > 0" class="flex flex-row items-center gap-2">
apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue (1)

7-13: Prefer locale-aware currency formatting.

Avoid manual "₽" concatenation; use Intl.NumberFormat to respect locale and spacing.

-          <h3 class="text-2xl/5 font-bold">
-            {{ averageToday }}
-          </h3>
-          <p class="text-lg/4">
-
-          </p>
+          <h3 class="text-2xl/5 font-bold">
+            {{ averageTodayFormatted }}
+          </h3>

Add in <script setup>:

const averageToday = ref(1635)
const averageTodayFormatted = computed(() =>
  new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(averageToday.value)
)
apps/atrium-telegram/app/components/flow/OrdersOnline.vue (1)

6-8: Handle undefined during initial render.

Render 0 (or a placeholder) before store hydration to avoid an empty h3.

-        <h3 class="text-2xl/5 font-bold">
-          {{ kitchenStore.todayData?.ordersForNow }}
+        <h3 class="text-2xl/5 font-bold">
+          {{ kitchenStore.todayData?.ordersForNow ?? 0 }}
apps/atrium-telegram/app/stores/user.ts (1)

114-117: Reuse existing finder to avoid duplicate lookup and keep API consistent

Use the store’s find() to DRY up and centralize user lookup.

-  function getAvatarUrl(userId: string): string | undefined {
-    const user = users.value.find((user) => user.id === userId)
-    return user?.avatarUrl ?? undefined
-  }
+  function getAvatarUrl(userId: string): string | undefined {
+    return find(userId)?.avatarUrl ?? undefined
+  }
apps/atrium-telegram/app/pages/tasks/index.vue (1)

11-14: Heading level change: verify page-level hierarchy

Switching to h1 is fine; ensure there’s a single h1 per page and surrounding sections maintain a logical heading order for a11y.

apps/atrium-telegram/app/components/NavigationButton.vue (1)

25-27: Badge visibility check and UChip color prop

  • Prefer numeric check to avoid edge cases with whitespace or non-numeric strings.
  • Confirm UChip supports color="error" in your UI library/version.
-        color="error"
-        :show="!!route.badge && route.badge !== '0'"
+        color="error"
+        :show="Number(route.badge) > 0"
apps/atrium-telegram/app/composables/useNavigation.ts (1)

16-16: Avoid flashing a large badge before user is known

When useUserStore().id is not yet available, nowViewedItemsCount counts all items, causing a distracting badge on the home route. Gate the count until user id is set (recommend fix in flow store).

In apps/atrium-telegram/app/stores/flow.ts:

const userId = computed(() => useUserStore().id)
const nowViewedItemsCount = computed(() => {
  if (!userId.value) return 0
  return items.value.filter((item) => !item.views.some((v) => v.userId === userId.value)).length
})
apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2)

17-29: Viewer avatars: minor UX polish

Consider showing a placeholder or “No views yet” state when item?.views.length === 0 to avoid an empty section.


47-51: Guard addView with user presence to avoid premature calls

Prevents firing before user id is resolved; still safe with idempotent server.

-watch(isViewed, () => {
-  if (!isViewed.value && item.value?.id) {
-    flowStore.addView(item.value.id)
-  }
-}, { immediate: true })
+watch(isViewed, () => {
+  if (userStore.id && !isViewed.value && item.value?.id) {
+    flowStore.addView(item.value.id)
+  }
+}, { immediate: true })
apps/atrium-telegram/app/stores/flow.ts (2)

34-54: Handle “already viewed” (400) and avoid noisy errors; consider lighter refresh.

  • Client ignores 400 from server (“Already viewed”); swallow it to prevent user-visible errors.
  • Optional: update the single item locally instead of refetching the whole list.

Apply:

   async function addView(itemId: string) {
     try {
       await $fetch(`/api/flow/id/${itemId}/view`, {
         method: 'POST',
         headers: {
           Authorization: `tma ${initDataRaw.value}`,
         },
       })
 
-      await update()
+      await update()
     } catch (error) {
-      if (error instanceof Error) {
-        if (error.message.includes('401')) {
-          // No
-        }
-        if (error.message.includes('404')) {
-          // Not found
-        }
-      }
+      if (error instanceof Error) {
+        // Nuxt $fetch often includes status code in message; adapt if you use a typed FetchError helper.
+        if (error.message.includes('400')) {
+          // Already viewed
+          return
+        }
+        if (error.message.includes('401')) {
+          // No
+          return
+        }
+        if (error.message.includes('404')) {
+          // Not found
+          return
+        }
+      }
+      // Optional: rethrow others or log
     }
   }

If you want the lighter refresh, instead of await update() append the new view to the matching item:

const myId = userId.value
const idx = items.value.findIndex(i => i.id === itemId)
if (idx !== -1 && myId) {
  items.value[idx] = { ...items.value[idx], views: [...(items.value[idx].views ?? []), { itemId, userId: myId }] }
}

59-63: Naming nit: “nowViewedItemsCount” reads like “viewed now.”

If it counts unviewed items, prefer unviewedItemsCount (or notViewedItemsCount) for clarity. Update consumers accordingly.

apps/atrium-telegram/app/components/Navigation.vue (2)

4-15: Wire the “plus” button and add an accessible label.

Button has no action or label; add @click and aria-label/title.

-      <UButton
+      <UButton
         v-if="isMainPage"
         variant="solid"
         color="secondary"
         size="xl"
         class="transition-all duration-200 ease-in-out motion-preset-slide-down motion-duration-200"
         icon="i-lucide-plus"
+        aria-label="Create"
+        title="Create"
+        @click="/* TODO: navigate to create flow item */"
         :ui="{
           base: 'size-12 font-bold rounded-full',
           leadingIcon: 'size-6 mx-auto',
         }"
       />

2-3: Verify h-38 exists in your CSS preset.

If using Tailwind default scale, h-38 isn’t standard; switch to h-[9.5rem] or confirm UnoCSS preset supports it.

apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)

23-28: Race-safe duplicate prevention.

Two concurrent requests can pass the check and insert twice. Enforce a DB unique (itemId,userId) and use an upsert/do-nothing; map conflict to 200/204 client response.

-    if (item.views.some((view) => view.userId === event.context.user.id)) {
+    if (item.views.some((view) => view.userId === event.context.user.id)) {
       throw createError({
         statusCode: 400,
         message: 'Already viewed',
       })
     }
@@
-    await repository.flow.createItemView({
+    await repository.flow.createItemView({
       itemId,
       userId: event.context.user.id,
     })

DB side (tables): add unique index (item_id, user_id) and make createItemView use onConflictDoNothing.

apps/atrium-telegram/app/components/flow/ItemCard.vue (1)

6-15: Micro‑UX: “Not viewed” block could be less jumpy.

Consider reducing motion or adding prefers-reduced-motion guards for accessibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 612b235 and 5a62ef8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • apps/atrium-telegram/app/components/EpicCard.vue (1 hunks)
  • apps/atrium-telegram/app/components/Navigation.vue (2 hunks)
  • apps/atrium-telegram/app/components/NavigationButton.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/FeedbackAverage.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/ItemCard.vue (2 hunks)
  • apps/atrium-telegram/app/components/flow/KitchensOnline.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/OrderAmountAverage.vue (1 hunks)
  • apps/atrium-telegram/app/components/flow/OrdersOnline.vue (1 hunks)
  • apps/atrium-telegram/app/composables/useNavigation.ts (2 hunks)
  • apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1 hunks)
  • apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (2 hunks)
  • apps/atrium-telegram/app/pages/tasks/index.vue (1 hunks)
  • apps/atrium-telegram/app/stores/flow.ts (2 hunks)
  • apps/atrium-telegram/app/stores/user.ts (2 hunks)
  • apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1 hunks)
  • apps/atrium-telegram/shared/types/index.ts (2 hunks)
  • package.json (1 hunks)
  • packages/database/src/repository/flow.ts (3 hunks)
  • packages/database/src/tables.ts (3 hunks)
  • packages/database/src/types.ts (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/atrium-telegram/app/stores/flow.ts (1)
apps/atrium-telegram/app/stores/user.ts (1)
  • useUserStore (8-141)
apps/atrium-telegram/app/stores/user.ts (1)
packages/database/src/tables.ts (1)
  • users (73-91)
apps/atrium-telegram/app/composables/useNavigation.ts (1)
apps/atrium-telegram/app/stores/flow.ts (1)
  • useFlowStore (3-64)
apps/atrium-telegram/shared/types/index.ts (1)
packages/database/src/types.ts (1)
  • FlowItemView (209-209)
packages/database/src/repository/flow.ts (2)
packages/database/src/types.ts (1)
  • FlowItemViewDraft (210-210)
packages/database/src/tables.ts (1)
  • flowItemViews (821-833)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Socket Security: Pull Request Alerts
  • GitHub Check: build
🔇 Additional comments (18)
apps/atrium-telegram/app/components/flow/KitchensOnline.vue (1)

13-15: Typography tweak LGTM.

No logic changes; visual scale aligns with adjacent Flow widgets.

apps/atrium-telegram/app/components/flow/FeedbackAverage.vue (1)

8-12: UI-only change LGTM.

Consistent scale with other Flow cards; no logic impact.

apps/atrium-telegram/app/components/EpicCard.vue (2)

5-7: Title scale bump LGTM.

Visual hierarchy reads better against the icon.


9-11: Description color: confirm readability intent.

Found same pattern (no text-muted) in:

  • apps/atrium-telegram/app/components/EpicCard.vue:9
  • apps/atrium-telegram/app/components/flow/ItemCard.vue:21

Dropping text-muted increases contrast — keep if intentional; otherwise restore text-muted to reduce visual noise in dense lists.

pnpm-workspace.yaml (1)

57-58: Regenerate pnpm lockfile and re-run full typecheck (install deps first).

pnpm-lock.yaml has no "nuxt" entries; typecheck failed with multiple TS2307/TS7006 errors in packages/geo-database because node_modules are missing. Run: pnpm -w install && git add pnpm-lock.yaml && pnpm -r run typecheck. Verify a single Nuxt version is locked.

package.json (1)

49-51: Incorrect: pnpm does not ignore Yarn resolutions — prefer pnpm.overrides for explicitness

pnpm merges package.json resolutions with pnpm.overrides (with pnpm.overrides taking precedence). pnpm-lock.yaml already resolves rollup@4.50.2, so no immediate fix is required; add or convert to a pnpm-specific override (pnpm.overrides or top-level overrides) only if you want explicit pnpm configuration.

Likely an incorrect or invalid review comment.

apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)

16-18: OK to keep this

— no app-level heading found in this app.

PageContainer.vue does not render an

, and no layouts/components in apps/atrium-telegram emit an

; only page files under apps/atrium-telegram/app/pages/* contain

, so this page-level heading is appropriate.

apps/atrium-telegram/app/stores/user.ts (1)

139-139: LGTM: exposing getAvatarUrl from the store

Public surface looks consistent with current usage in Flow item view.

packages/database/src/tables.ts (1)

1312-1313: Relations for views look correct

flowItemRelations.views and flowItemViewRelations wiring is consistent with the schema.

Also applies to: 1326-1335

apps/atrium-telegram/shared/types/index.ts (2)

1-1: Imports updated correctly

FlowItemView import aligns with DB types.


13-13: Augmenting FlowItemWithData with views is consistent

Type shape matches repository eager-loading changes.

packages/database/src/repository/flow.ts (2)

10-12: Eager-loading views is appropriate

Ensures client types are fulfilled without extra roundtrips.


34-37: Make createItemView idempotent (DB-level conflict handling)

Add DB-level conflict handling on (userId, itemId) and keep returning() so callers get the created row or undefined on conflict.

File: packages/database/src/repository/flow.ts (lines 34–37)

  static async createItemView(data: FlowItemViewDraft) {
-    const [view] = await useDatabase().insert(flowItemViews).values(data).returning()
-    return view
+    const [view] = await useDatabase()
+      .insert(flowItemViews)
+      .values(data)
+      .onConflictDoNothing({ target: [flowItemViews.userId, flowItemViews.itemId] })
+      .returning()
+    return view
  }
  • Ensure a UNIQUE constraint/index exists on (userId, itemId).
  • Confirm no callers rely on a non-undefined return; if they do, change the method to return void or { created: boolean }.
apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (1)

8-11: Heading level change: fine

Title promoted to h1; consistent with the broader typography pass.

packages/database/src/types.ts (1)

209-210: Types for FlowItemView look correct and consistent with existing patterns.

Matches the FlowItemComment pattern and will type repo/relations cleanly.

apps/atrium-telegram/app/components/Navigation.vue (1)

34-35: Script import hint (if needed).

If useNavigation gained isMainPage, ensure its export type is updated and consumers compiled. Looks fine otherwise.

apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts (1)

35-39: Error translation dependency.

Confirm errorResolver is auto-imported server-side; otherwise import it to avoid runtime ReferenceError.

[suggest_minor_issue]

apps/atrium-telegram/app/components/flow/ItemCard.vue (1)

17-18: Heading size change acknowledged.

Visual change to text-xl/5 looks consistent with the new header spacing.

Comment on lines +50 to 52
const userStore = useUserStore()
const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id))
</script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Ref-vs-string comparison in isViewed.

userStore.id is a Ref; compare against its value.

-const userStore = useUserStore()
-const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id))
+const userStore = useUserStore()
+const { id: userId } = storeToRefs(userStore)
+const isViewed = computed(() => item.views?.some((view) => view.userId === userId.value))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userStore = useUserStore()
const isViewed = computed(() => item.views.some((view) => view.userId === userStore?.id))
</script>
const userStore = useUserStore()
const { id: userId } = storeToRefs(userStore)
const isViewed = computed(() => item.views?.some((view) => view.userId === userId.value))
</script>
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/flow/ItemCard.vue around lines 50-52, the
computed isViewed compares item.views' userId to userStore.id which is a Ref;
change the comparison to use the Ref's inner value (e.g. use userStore.id.value
or unref(userStore.id)) so the equality checks the actual id string/number
rather than the Ref object, and guard with optional chaining if userStore or id
may be undefined.

Comment on lines +6 to +7
const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Ref-vs-string comparison in computed (using user id).

useUserStore().id is a Ref; you’re comparing string === Ref, which will fail. Bind the store once and unref the id.

Apply:

-  const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)
+  const userStore = useUserStore()
+  const { id: userId } = storeToRefs(userStore)
+  const nowViewedItemsCount = computed(() =>
+    items.value.filter((item) => !item.views?.some((view) => view.userId === userId.value)).length
+  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nowViewedItemsCount = computed(() => items.value.filter((item) => !item.views.some((view) => view.userId === useUserStore().id)).length)
const userStore = useUserStore()
const { id: userId } = storeToRefs(userStore)
const nowViewedItemsCount = computed(() =>
items.value.filter((item) => !item.views?.some((view) => view.userId === userId.value)).length
)
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/flow.ts around lines 6-7, the computed
predicate compares a string to a Ref (useUserStore().id) which always fails;
cache the store outside the computed (e.g., const user = useUserStore()) and
inside the computed compare against the unwrapped id (user.id.value or
unref(user.id)) so the filter checks string === string and avoid calling
useUserStore() repeatedly.

Comment on lines +5 to +12
const itemId = getRouterParam(event, 'itemId')
if (!itemId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add auth guard to avoid NPE and return 401 when unauthenticated.

event.context.user may be undefined; current code would 500 on property access.

   const itemId = getRouterParam(event, 'itemId')
   if (!itemId) {
     throw createError({
       statusCode: 400,
       message: 'Id is required',
     })
   }
+  if (!event.context.user?.id) {
+    throw createError({
+      statusCode: 401,
+      message: 'Unauthorized',
+    })
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const itemId = getRouterParam(event, 'itemId')
if (!itemId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}
const itemId = getRouterParam(event, 'itemId')
if (!itemId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}
if (!event.context.user?.id) {
throw createError({
statusCode: 401,
message: 'Unauthorized',
})
}
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/id/[itemId]/view.post.ts around lines
5-12, add an authentication guard that checks event.context?.user before any
property access and return a 401 when missing; specifically, before using itemId
or any user properties, if event.context?.user is falsy throw createError({
statusCode: 401, message: 'Unauthorized' }); then proceed with the existing
itemId validation (400) as before so you avoid null/undefined property access
and correctly signal unauthenticated requests.

Comment on lines +821 to +834
export const flowItemViews = pgTable('flow_item_views', {
id: cuid2('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
userId: cuid2('user_id').notNull().references(() => users.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
onDelete: 'cascade',
onUpdate: 'cascade',
}),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Make item views idempotent: add unique constraint (userId, itemId) and helpful indexes

Without a uniqueness guard, repeated POSTs (watchers, retries) will create duplicate views. Add a composite unique index and indexes for joins/filtering.

 export const flowItemViews = pgTable('flow_item_views', {
   id: cuid2('id').defaultRandom().primaryKey(),
   createdAt: timestamp('created_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
   updatedAt: timestamp('updated_at', { precision: 3, withTimezone: true, mode: 'string' }).notNull().defaultNow(),
   userId: cuid2('user_id').notNull().references(() => users.id, {
     onDelete: 'cascade',
     onUpdate: 'cascade',
   }),
   itemId: cuid2('item_id').notNull().references(() => flowItems.id, {
     onDelete: 'cascade',
     onUpdate: 'cascade',
   }),
-})
+}, (t) => ({
+  uiUserItem: uniqueIndex('ui_flow_item_views_user_item').on(t.userId, t.itemId),
+  idxItemId: index('idx_flow_item_views_item_id').on(t.itemId),
+  idxUserId: index('idx_flow_item_views_user_id').on(t.userId),
+}))

Add missing imports (outside this hunk):

// at the pg-core import
import { ..., index, uniqueIndex } from 'drizzle-orm/pg-core'
🤖 Prompt for AI Agents
In packages/database/src/tables.ts around lines 821 to 834, the flow_item_views
table allows duplicate rows for the same userId and itemId and lacks helpful
indexes; add a composite unique constraint on (userId, itemId) to make views
idempotent and add non-unique indexes on userId and itemId to speed
joins/filters, and update the pg-core import to include index and uniqueIndex
from 'drizzle-orm/pg-core'.

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