feat: epic pages and cards#102
Conversation
WalkthroughAdds an Epic feature end-to-end: shared schemas, Pinia store, server APIs (list/create/update, comment create/delete), UI components (cards, comments, forms, modals), and pages for epic list/detail. Integrates epic store updates into app startup/interval loops. Introduces layout components (Section, ActiveCard) and adjusts navigation active matching and minor UI spacings/animations. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant App as App (Nuxt)
participant Store as EpicStore
participant API as Server API
participant Repo as Repository
Note over App,Store: App startup / periodic refresh
U->>App: Open app
App->>Store: epic.update()
Store->>API: GET /api/epic/list (auth)
API->>Repo: epic.list()
Repo-->>API: Epics[]
API-->>Store: Epics with comments/notifications
Store-->>App: Update state
Note over U,App: Create Epic via modal
U->>App: Open Create Epic modal
U->>API: POST /api/epic (auth, body)
API->>Repo: epic.create(...)
Repo-->>API: Epic
API-->>U: { ok: true, result }
App->>Store: epic.update()
Note over U,App: Update Epic via modal
U->>API: PATCH /api/epic/id/:epicId (auth, body)
API->>Repo: epic.update(...)
Repo-->>API: Epic
API-->>U: { ok: true, result }
App->>Store: epic.update()
sequenceDiagram
autonumber
actor U as User
participant App as Epic Detail Page
participant Store as EpicStore
participant API as Server API
participant Repo as Repository
Note over U,App: Add comment
U->>App: Submit comment text
App->>Store: addComment(epicId, text)
Store->>API: POST /api/epic/id/:epicId/comment
API->>Repo: epic.createComment(...)
Repo-->>API: Comment
API-->>Store: { ok: true }
Store->>API: GET /api/epic/list
API-->>Store: Epics[]
Store-->>App: Refresh detail
Note over U,App: Delete own comment
U->>API: DELETE /api/epic/comment/id/:commentId
API->>Repo: epic.deleteComment(...)
Repo-->>API: { ok: true }
API-->>U: { ok: true }
App->>Store: epic.update()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/atrium-telegram/app/app.vue (1)
71-77: Make startup updates resilient to a single store failure (use allSettled).
One rejected promise will currently block the rest and the interval setup.Apply:
-onMounted(async () => { - await Promise.all([ +onMounted(async () => { + await Promise.allSettled([ user.updateOnline(), user.update(), task.update(), epic.update(), ])
🧹 Nitpick comments (20)
apps/atrium-telegram/app/components/Navigation.vue (2)
22-23: Reuse the same active helper for label styling.Prevents duplication and keeps logic consistent with the icon container.
- (route.exact ? router.currentRoute.value.path === route.path : router.currentRoute.value.path.startsWith(route.path)) && 'tg-text', + isActive(route) && 'tg-text',
47-51: i18n and naming consistency.
- Title 'Эпики' is hardcoded; prefer i18n key (e.g.,
t('app.epic.list')).name: 'quests'doesn’t match path/epic; considername: 'epic'.apps/atrium-telegram/shared/services/epic.ts (2)
5-6: Remove redundant| undefinedwhen also using.optional().Keeps schemas precise and avoids allowing explicit
undefinedpayloads.- description: type('string <= 1500 | undefined').describe('error.length.invalid').optional(), + description: type('string <= 1500').describe('error.length.invalid').optional(),
10-12: Same redundancy in update schema.Make each field optional, but validate only the concrete string values.
- title: type('2 <= string <= 150 | undefined').describe('error.length.invalid').optional(), - description: type('string <= 1500 | undefined').describe('error.length.invalid').optional(), + title: type('2 <= string <= 150').describe('error.length.invalid').optional(), + description: type('string <= 1500').describe('error.length.invalid').optional(),apps/atrium-telegram/app/components/modal/CreateEpic.vue (1)
1-7: Close only on success to avoid hiding validation errors.
Closing on submitted can dismiss the modal before an API error is shown.Apply:
- <FormCreateEpic @submitted="overlay.closeAll" @success="overlay.closeAll" /> + <FormCreateEpic @success="overlay.closeAll" />apps/atrium-telegram/app/components/modal/CreateEpicCommentBeacon.vue (1)
4-8: Mirror create-modal behavior: close on success only.
Prevents accidental dismissal on failures.Apply:
- @submitted="overlay.closeAll" - @success="overlay.closeAll" + @success="overlay.closeAll"apps/atrium-telegram/app/pages/epic/index.vue (2)
9-15: Reduce network churn when many items by disabling link prefetch.
Useful if epics list grows.Apply:
- <NuxtLink + <NuxtLink + prefetch="false" v-for="epic of epicStore.epics" :key="epic.id" :to="`/epic/${epic.id}`" >
23-30: Consider an empty/skeleton state.
Show placeholder when epicStore.epics is empty/loading.apps/atrium-telegram/server/api/epic/list.get.ts (2)
3-13: Add error handling and minimal logging.
Prevent leaking stack traces and aid observability.Apply:
-export default defineEventHandler(async (event) => { +export default defineEventHandler(async (event) => { const user = event.context.user if (!user) { throw createError({ statusCode: 401, message: 'Not logged in', }) } - - return repository.epic.list() + try { + return await repository.epic.list() + } catch (err) { + // TODO: plug logger + throw createError({ statusCode: 500, message: 'Failed to fetch epics' }) + } })
1-13: Consider ETag/conditional GETs for 30s polling.
Will cut payload and DB load when nothing changed.apps/atrium-telegram/server/api/epic/id/[epicId]/comment.post.ts (1)
9-13: Clarify 400 message for missing route param.Use the param name for better debugging.
- throw createError({ - statusCode: 400, - message: 'Id is required', - }) + throw createError({ + statusCode: 400, + message: 'epicId is required', + })apps/atrium-telegram/app/components/form/CreateEpicComment.vue (1)
10-22: Enforce max length on the client and localize the placeholder.Keeps UX aligned with server (<=1500) and prepares for i18n.
<UTextarea v-model="text" color="neutral" variant="none" size="xl" required autoresize - placeholder="Напишите свою мысль..." + :placeholder="$t('epic.comment.placeholder')" :rows="3" + :maxlength="1500" :disabled="loading" class="w-full" :ui="{ base: 'p-1' }" />apps/atrium-telegram/server/api/epic/index.post.ts (1)
37-46: Parallelize notifications with Promise.all (or a batch create).Sends notifications faster and avoids serial IO.
- for (const staff of allStaffExceptUser) { - await repository.notification.create({ - authorId: user.id, - userId: staff.id, - epicId: epic.id, - type: 'epic_created', - title: `${suffixByGender(['Создал', 'Создала'], user.gender)} эпик «${epic.title}»`, - description: epic.description ? epic.description : 'Без описания', - }) - } + await Promise.all( + allStaffExceptUser.map((staff) => + repository.notification.create({ + authorId: user.id, + userId: staff.id, + epicId: epic.id, + type: 'epic_created', + title: `${suffixByGender(['Создал', 'Создала'], user.gender)} эпик «${epic.title}»`, + description: epic.description ? epic.description : 'Без описания', + }), + ), + )apps/atrium-telegram/app/components/EpicCard.vue (1)
15-18: Nit: optional chaining on required prop.
epicis a required prop;epic?.comments.lengthcan beepic.comments.length.- <p>{{ epic?.comments.length }}</p> + <p>{{ epic.comments.length }}</p>apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)
56-58: Pass the known route param to the modal instead of a possibly-empty epic id.This removes a race with store loading and avoids sending an empty id.
-function handleEditEpic() { - modalUpdateEpic.open({ epicId: epic.value?.id ?? '' }) -} +function handleEditEpic() { + modalUpdateEpic.open({ epicId: params.epicId }) +}apps/atrium-telegram/app/components/EpicComment.vue (1)
77-77: Pass epicId to the beacon modal.The modal likely needs both ids; currently only
commentIdis sent.- onSelect: () => modalCreateEpicCommentBeacon.open({ commentId }), + onSelect: () => modalCreateEpicCommentBeacon.open({ epicId, commentId }),apps/atrium-telegram/app/stores/epic.ts (4)
4-10: Export EpicCommentWithData for reuse across components.This helps type consumers that need comment notifications.
-type EpicCommentWithData = EpicComment & { +export type EpicCommentWithData = EpicComment & { notifications: Notification[] }
41-53: Same: guard auth and type body for addComment.Prevents unauthenticated calls and clarifies payload shape.
async function addComment(epicId: string, text: string) { try { + if (!initDataRaw.value) return await $fetch(`/api/epic/id/${epicId}/comment`, { method: 'POST', headers: { Authorization: `tma ${initDataRaw.value}`, }, body: { text, }, }) await update()
66-75: Same: guard auth for removeComment.async function removeComment(id: string) { try { + if (!initDataRaw.value) return await $fetch(`/api/epic/comment/id/${id}`, { method: 'DELETE', headers: { Authorization: `tma ${initDataRaw.value}`, }, })
28-39: Improve error handling with status-based branching.String-matching on messages is brittle; prefer status codes from
ofetcherrors.I can refactor to use
isFetchError(e)/(e as FetchError).response?.statusand surface toasts.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (26)
apps/atrium-telegram/app/app.vue(2 hunks)apps/atrium-telegram/app/components/ActiveCard.vue(1 hunks)apps/atrium-telegram/app/components/CreateCard.vue(1 hunks)apps/atrium-telegram/app/components/EpicCard.vue(1 hunks)apps/atrium-telegram/app/components/EpicComment.vue(1 hunks)apps/atrium-telegram/app/components/Navigation.vue(2 hunks)apps/atrium-telegram/app/components/PageContainer.vue(1 hunks)apps/atrium-telegram/app/components/Section.vue(1 hunks)apps/atrium-telegram/app/components/TaskCard.vue(1 hunks)apps/atrium-telegram/app/components/TaskList.vue(2 hunks)apps/atrium-telegram/app/components/form/CreateEpicComment.vue(1 hunks)apps/atrium-telegram/app/components/modal/CreateEpic.vue(1 hunks)apps/atrium-telegram/app/components/modal/CreateEpicCommentBeacon.vue(1 hunks)apps/atrium-telegram/app/components/modal/UpdateEpic.vue(1 hunks)apps/atrium-telegram/app/pages/epic.vue(0 hunks)apps/atrium-telegram/app/pages/epic/[epicId]/index.vue(1 hunks)apps/atrium-telegram/app/pages/epic/index.vue(1 hunks)apps/atrium-telegram/app/pages/index.vue(1 hunks)apps/atrium-telegram/app/stores/epic.ts(1 hunks)apps/atrium-telegram/i18n/locales/ru-RU.json(2 hunks)apps/atrium-telegram/server/api/epic/comment/id/[commentId]/index.delete.ts(1 hunks)apps/atrium-telegram/server/api/epic/id/[epicId]/comment.post.ts(1 hunks)apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts(1 hunks)apps/atrium-telegram/server/api/epic/index.post.ts(1 hunks)apps/atrium-telegram/server/api/epic/list.get.ts(1 hunks)apps/atrium-telegram/shared/services/epic.ts(1 hunks)
💤 Files with no reviewable changes (1)
- apps/atrium-telegram/app/pages/epic.vue
🧰 Additional context used
🧬 Code graph analysis (4)
apps/atrium-telegram/server/api/epic/id/[epicId]/comment.post.ts (1)
apps/atrium-telegram/shared/services/epic.ts (1)
createEpicCommentSchema(15-17)
apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts (1)
apps/atrium-telegram/shared/services/epic.ts (1)
updateEpicSchema(9-12)
apps/atrium-telegram/app/stores/epic.ts (2)
packages/database/src/types.ts (1)
EpicComment(168-168)packages/database/src/tables.ts (1)
epics(708-718)
apps/atrium-telegram/server/api/epic/index.post.ts (2)
apps/atrium-telegram/shared/services/epic.ts (1)
createEpicSchema(3-6)packages/database/src/tables.ts (1)
users(75-93)
⏰ 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 (16)
apps/atrium-telegram/app/components/PageContainer.vue (1)
3-3: LGTM! Improved bottom spacing for enhanced layout.The addition of
mb-20provides better spacing at the bottom of the page container, which aligns well with the new Section-based layout components introduced in this PR.apps/atrium-telegram/app/components/Section.vue (1)
1-5: LGTM! Clean and reusable layout component.This is a well-designed layout component that provides consistent styling across the application. The use of Tailwind CSS classes and the group/section utility for potential child element styling is appropriate. The component follows Vue 3 composition API best practices with a simple slot-based design.
apps/atrium-telegram/app/components/TaskList.vue (1)
2-2: LGTM! Consistent adoption of the new Section component.The migration from a manual div wrapper to the Section component maintains the same styling while improving consistency across the application. This change aligns perfectly with the new layout pattern established in this PR.
Also applies to: 90-90
apps/atrium-telegram/app/components/TaskCard.vue (1)
39-39: LGTM! Enhanced user experience with smooth animations.The addition of
motion-preset-slide-left motion-duration-200classes to dropdown menu items provides a polished sliding animation that enhances the user interface. The 200ms duration is appropriate for responsive feel without being too slow.apps/atrium-telegram/app/pages/index.vue (1)
36-38: LGTM! Consistent Section component adoption and improved spacing.The replacement of the manual div wrapper with the Section component maintains consistent styling while following the new layout pattern. The removal of the trailing
<div class="mb-20" />spacer is also appropriate since the PageContainer now handles bottom margin withmb-20.apps/atrium-telegram/i18n/locales/ru-RU.json (2)
38-41: Add matching keys to other locales or provide fallbacks.The new create-epic strings look good. Ensure en/other locales define
app.create.epic.button|titleto avoid mixed-language UI.
58-61: Mirror update-epic keys across locales.Add
app.update.epic.button|titleto all supported locales, or confirm fallback strategy.apps/atrium-telegram/app/components/CreateCard.vue (1)
2-2: LGTM: reduced min height aligns with denser card grid.No functional impact; check that touch targets remain ≥44px on small screens.
apps/atrium-telegram/app/app.vue (1)
58-61: Epic store is correctly integrated into the app store bootstrap.
Looks consistent with existing user/task patterns.apps/atrium-telegram/app/components/modal/CreateEpic.vue (1)
9-11: Confirm component auto-registration.
Assuming FormCreateEpic is auto-registered; otherwise import it.apps/atrium-telegram/app/components/modal/CreateEpicCommentBeacon.vue (1)
14-16: Validate prop type against backend ID type.
If comment IDs are numeric/UUID, keep as string; otherwise adjust.apps/atrium-telegram/app/pages/epic/index.vue (1)
1-21: Solid page scaffold; list + create affordance looks good.
Clean layout; proper keys on list.apps/atrium-telegram/server/api/epic/list.get.ts (1)
12-13: Verify tenant scoping to the authenticated user/workspace.
Calling repository.epic.list() without user context may expose unrelated data unless the repo scopes internally.If scoping is not internal, pass user identifiers (e.g., workspaceId/userId) to the repository.
apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)
26-29: Fix comment count check and verify helper availability
- Change the v-if to
v-if="epic?.comments?.length > 0"(instead of>= 0).- Ensure
pluralizationRuis either explicitly imported in this component or auto-registered (e.g. via your Nuxt/Vite auto-import setup).apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts (1)
29-34: Response shape LGTM.Consistent
{ ok, result }contract.apps/atrium-telegram/app/components/EpicComment.vue (1)
70-105: Type-safe menu items with conditional rendering & fixed disabled logicDefine a local
ConditionalMenuItemto includecondition, filter and strip it before returning, and enable actions when shown. Update the beacon call to include bothepicIdandcommentId.import type { DropdownMenuItem } from '@nuxt/ui' @@ -type items = computed<DropdownMenuItem[]>(() => { - const menuItems: DropdownMenuItem[] = [ +type ConditionalMenuItem = DropdownMenuItem & { condition?: boolean } + +const items = computed<DropdownMenuItem[]>(() => { + const menuItems: ConditionalMenuItem[] = [ { label: 'Маякнуть', icon: 'i-lucide-users-round', color: 'neutral', - disabled: false, - onSelect: () => modalCreateEpicCommentBeacon.open({ commentId }), + disabled: false, + onSelect: () => modalCreateEpicCommentBeacon.open({ epicId, commentId }), condition: true, }, { label: 'Поставить лайк', icon: 'i-lucide-thumbs-up', color: 'neutral', - disabled: true, + disabled: false, // only disabled when it’s your own comment onSelect: () => {}, condition: user.value?.id !== userStore.id, }, { label: 'Редактировать', icon: 'i-lucide-edit', - disabled: true, + disabled: false, onSelect: () => {}, condition: user.value?.id === userStore.id, }, { label: 'Удалить', icon: 'i-lucide-trash-2', - disabled: true, + disabled: false, onSelect: () => {}, condition: user.value?.id === userStore.id, }, ] - return menuItems.filter((item) => item.condition) + return menuItems + .filter((item) => item.condition !== false) + .map(({ condition, ...item }) => item) })Please confirm that
userStore.idindeed returns the current authenticated user’s ID. If the store nests the user under another key (e.g.userStore.user.id), adjust the condition accordingly.
| interval = setInterval(async () => { | ||
| await Promise.all([ | ||
| user.updateOnline(), | ||
| user.update(), | ||
| task.update(), | ||
| epic.update(), | ||
| ]) | ||
| }, 30000) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Harden periodic updates and avoid type pitfall for interval handle.
- Same resilience issue as above.
- In browser, prefer ReturnType over NodeJS.Timeout to avoid TS friction.
Apply:
- interval = setInterval(async () => {
- await Promise.all([
+ interval = setInterval(async () => {
+ await Promise.allSettled([
user.updateOnline(),
user.update(),
task.update(),
epic.update(),
])
}, 30000)Additionally (outside this hunk), change the declaration:
// Before: let interval: NodeJS.Timeout
let interval: ReturnType<typeof setInterval>🏁 Script executed:
#!/bin/bash
rg -n "let interval" -C3 apps/atrium-telegram/app/app.vueLength of output: 181
Harden periodic updates and use correct interval type
Replace Promise.all with Promise.allSettled in the interval callback so one failed update doesn’t cancel the others, and change the interval’s declaration from NodeJS.Timeout to the browser-compatible ReturnType<typeof setInterval>.
--- a/apps/atrium-telegram/app/app.vue
@@ -79,7 +79,7 @@ onMounted(async () => {
interval = setInterval(async () => {
- await Promise.all([
+ await Promise.allSettled([
user.updateOnline(),
user.update(),
task.update(),
epic.update(),
])
}, 30000)And at line 69:
- let interval: NodeJS.Timeout
+ let interval: ReturnType<typeof setInterval>📝 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.
| interval = setInterval(async () => { | |
| await Promise.all([ | |
| user.updateOnline(), | |
| user.update(), | |
| task.update(), | |
| epic.update(), | |
| ]) | |
| }, 30000) | |
| // use a browser-compatible timer handle | |
| let interval: ReturnType<typeof setInterval> | |
| onMounted(async () => { | |
| // use allSettled so one rejection doesn’t short-circuit the rest | |
| interval = setInterval(async () => { | |
| await Promise.allSettled([ | |
| user.updateOnline(), | |
| user.update(), | |
| task.update(), | |
| epic.update(), | |
| ]) | |
| }, 30000) | |
| }) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/app.vue around lines 69 and 79-86, the periodic
update uses Promise.all and the interval is typed as NodeJS.Timeout which is not
browser-compatible; change the interval variable's type from NodeJS.Timeout to
ReturnType<typeof setInterval> (at/near line 69) and replace Promise.all with
Promise.allSettled inside the setInterval callback so one rejection doesn't
cancel the other updates; after awaiting Promise.allSettled, optionally inspect
results and log or handle rejected entries to surface failures without breaking
the other updates.
| <div class="relative active:scale-90 duration-200 motion-preset-slide-left" @click="vibrate()"> | ||
| <Section> | ||
| <slot /> | ||
| </Section> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Make the clickable wrapper accessible (keyboard + ARIA).
The root div is clickable but not keyboard-accessible. Add role/tabindex and handle Enter/Space.
- <div class="relative active:scale-90 duration-200 motion-preset-slide-left" @click="vibrate()">
+ <div
+ role="button"
+ tabindex="0"
+ class="relative active:scale-90 duration-200 motion-preset-slide-left"
+ @click="vibrate()"
+ @keydown.enter.prevent="vibrate()"
+ @keydown.space.prevent="vibrate()"
+ >
<Section>
<slot />
</Section>
</div>📝 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.
| <div class="relative active:scale-90 duration-200 motion-preset-slide-left" @click="vibrate()"> | |
| <Section> | |
| <slot /> | |
| </Section> | |
| </div> | |
| <div | |
| role="button" | |
| tabindex="0" | |
| class="relative active:scale-90 duration-200 motion-preset-slide-left" | |
| @click="vibrate()" | |
| @keydown.enter.prevent="vibrate()" | |
| @keydown.space.prevent="vibrate()" | |
| > | |
| <Section> | |
| <slot /> | |
| </Section> | |
| </div> |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/ActiveCard.vue around lines 2 to 6, the
root div is clickable but not keyboard-accessible; add accessibility attributes
and keyboard handling by giving the div role="button" and tabindex="0" and
wiring a keydown handler that invokes the same vibrate() method when Enter or
Space are pressed (for Space, prevent default to avoid page scroll); keep the
existing @click handler and ensure any ARIA label/aria-labelledby is provided by
the consumer if necessary.
| <time | ||
| :datetime="epic.createdAt" | ||
| class="text-sm text-muted" | ||
| v-text="format(new Date(epic.createdAt), 'от d MMMM yyyy', { locale: ru })" | ||
| /> |
There was a problem hiding this comment.
Escape literal text in date-fns format string.
'o' is a token; without quotes you'll get incorrect output. Wrap "от" in quotes.
- v-text="format(new Date(epic.createdAt), 'от d MMMM yyyy', { locale: ru })"
+ v-text="format(new Date(epic.createdAt), `'от' d MMMM yyyy`, { locale: ru })"📝 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.
| <time | |
| :datetime="epic.createdAt" | |
| class="text-sm text-muted" | |
| v-text="format(new Date(epic.createdAt), 'от d MMMM yyyy', { locale: ru })" | |
| /> | |
| <time | |
| :datetime="epic.createdAt" | |
| class="text-sm text-muted" | |
| v-text="format( | |
| new Date(epic.createdAt), | |
| `'от' d MMMM yyyy`, | |
| { locale: ru } | |
| )" | |
| /> |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/EpicCard.vue around lines 21 to 25, the
date-fns format string uses unquoted literal text "от" which will be interpreted
as a token; update the format string to escape the literal by wrapping it in
quotes (e.g. "'от' d MMMM yyyy") so the word "от" is rendered literally when
formatting epic.createdAt.
| import { format } from 'date-fns' | ||
| import { ru } from 'date-fns/locale/ru' |
There was a problem hiding this comment.
Fix date-fns locale import (named import from locale/ru is invalid).
Use the default export from 'date-fns/locale/ru' to avoid runtime undefined.
-import { format } from 'date-fns'
-import { ru } from 'date-fns/locale/ru'
+import { format } from 'date-fns'
+import ru from 'date-fns/locale/ru'📝 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.
| import { format } from 'date-fns' | |
| import { ru } from 'date-fns/locale/ru' | |
| import { format } from 'date-fns' | |
| import ru from 'date-fns/locale/ru' |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/EpicCard.vue around lines 32-33, the code
uses a named import for the Russian locale (import { ru } from
'date-fns/locale/ru') which is invalid and yields undefined at runtime; change
it to use the default import (import ru from 'date-fns/locale/ru') and keep
passing ru to format/locale options, ensuring any references to ru remain the
same.
| import { format } from 'date-fns' | ||
| import { ru } from 'date-fns/locale/ru' | ||
|
|
There was a problem hiding this comment.
Fix date-fns locale import (named import from locale/ru is invalid).
Same issue as in EpicCard.vue.
-import { format } from 'date-fns'
-import { ru } from 'date-fns/locale/ru'
+import { format } from 'date-fns'
+import ru from 'date-fns/locale/ru'📝 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.
| import { format } from 'date-fns' | |
| import { ru } from 'date-fns/locale/ru' | |
| import { format } from 'date-fns' | |
| import ru from 'date-fns/locale/ru' |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/EpicComment.vue around lines 52-54 the
locale import from date-fns is wrong: change the named import "import { ru }
from 'date-fns/locale/ru'" to a default import "import ru from
'date-fns/locale/ru'" (same fix used in EpicCard.vue) and ensure any format
calls pass the locale as { locale: ru }.
| const user = event.context.user | ||
| if (!user) { | ||
| throw createError({ | ||
| statusCode: 401, | ||
| message: 'Not logged in', | ||
| }) | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Authorization gap: any logged-in user can update any epic.
Enforce ownership (or role-based access) before updating to prevent privilege escalation.
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: 'Not logged in',
})
}
+
+ // Ensure only the owner (or authorized roles) can update this epic
+ const existing = await repository.epic.findById(epicId)
+ if (!existing) {
+ throw createError({ statusCode: 404, message: 'Epic not found' })
+ }
+ if (existing.userId !== user.id) {
+ throw createError({ statusCode: 403, message: 'Forbidden' })
+ }🏁 Script executed:
#!/bin/bash
# Discover available repository methods to use for the ownership check
rg -nP "repository\.epic\." packages apps | sed -n '1,120p'Length of output: 1719
Enforce epic ownership before update
After verifying the user is logged in, load the epic and check its userId against user.id to prevent any logged-in user from updating another user’s epic:
const existing = await repository.epic.find(epicId)
if (!existing) throw createError({ statusCode: 404, message: 'Epic not found' })
if (existing.userId !== user.id) throw createError({ statusCode: 403, message: 'Forbidden' })🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts around lines
15 to 21, after confirming event.context.user exists, load the epic with await
repository.epic.find(epicId) (using the route param epicId), throw createError({
statusCode: 404, message: 'Epic not found' }) if not found, then compare
existing.userId !== user.id and if they differ throw createError({ statusCode:
403, message: 'Forbidden' }) to enforce ownership before allowing updates.
| const body = await readBody(event) | ||
| const data = updateEpicSchema(body) | ||
| if (data instanceof type.errors) { | ||
| throw data | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use arktype assert for clearer validation flow.
Avoid sentinel checks; let assert throw a typed error which your resolver can handle.
- const body = await readBody(event)
- const data = updateEpicSchema(body)
- if (data instanceof type.errors) {
- throw data
- }
+ const body = await readBody(event)
+ const data = updateEpicSchema.assert(body)📝 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.
| const body = await readBody(event) | |
| const data = updateEpicSchema(body) | |
| if (data instanceof type.errors) { | |
| throw data | |
| } | |
| const body = await readBody(event) | |
| const data = updateEpicSchema.assert(body) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts around lines
23 to 27, replace the sentinel-style validation (assigning
updateEpicSchema(body) to data and checking `data instanceof type.errors`) with
arktype's assert pattern: call the schema's assert function directly on the
parsed body so it throws a typed validation error on failure and returns the
validated value on success; remove the `if (data instanceof type.errors) { throw
data }` branch and use the assert-returned value as your `data` for the rest of
the handler.
| } catch (error) { | ||
| throw errorResolver(error) | ||
| } |
There was a problem hiding this comment.
Missing import for error resolver.
errorResolver is used but not imported; this will throw at runtime.
-import { updateEpicSchema } from '#shared/services/epic'
+import { updateEpicSchema } from '#shared/services/epic'
+import { errorResolver } from '#shared/utils/error' // adjust path if different📝 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.
| } catch (error) { | |
| throw errorResolver(error) | |
| } | |
| // apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts | |
| import { NextApiRequest, NextApiResponse } from 'next' | |
| import { withMethods } from '#shared/lib/api-middleware/with-methods' | |
| import { updateEpicSchema } from '#shared/services/epic' | |
| import { errorResolver } from '#shared/utils/error' // adjust path if different | |
| import prisma from '#shared/lib/prisma' | |
| import { revalidate } from '#shared/lib/revalidate' | |
| // …rest of file… | |
| } catch (error) { | |
| throw errorResolver(error) | |
| } |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/epic/id/[epicId]/index.patch.ts around lines
35–37, errorResolver is thrown but not imported; add the appropriate import at
the top of the file referencing the module that exports errorResolver (use the
correct relative path and match named vs default export), e.g. add an import
statement for errorResolver alongside the other imports and ensure
linter/formatter ordering is respected so the runtime can find and call
errorResolver in the catch block.
| // Notify all staff | ||
| if (user.type === 'staff') { | ||
| const users = await repository.user.list() | ||
| const allStaffExceptUser = users.filter((u) => u.type === 'staff' && u.id !== user.id) | ||
|
|
||
| for (const staff of allStaffExceptUser) { | ||
| await repository.notification.create({ | ||
| authorId: user.id, | ||
| userId: staff.id, | ||
| epicId: epic.id, | ||
| type: 'epic_created', | ||
| title: `${suffixByGender(['Создал', 'Создала'], user.gender)} эпик «${epic.title}»`, | ||
| description: epic.description ? epic.description : 'Без описания', | ||
| }) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify suffixByGender availability/import.
It’s used but not imported here. If not globally auto-imported, add an explicit import.
Run to verify declaration/imports:
If it’s not auto-imported, add an import (adjust path to your utilities module):
import { type } from 'arktype'
+import { suffixByGender } from '#shared/utils/suffixByGender'🏁 Script executed:
#!/bin/bash
# Find suffixByGender definition and imports across the repo
rg -n -C2 -S '\bexport (const|function)\s+suffixByGender\b|\bsuffixByGender\s*\('Length of output: 7096
Add missing import of suffixByGender
At the top of apps/atrium-telegram/server/api/epic/index.post.ts, add:
import { suffixByGender } from '../../../shared/utils/helpers'🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/epic/index.post.ts around lines 32 to 45, the
function suffixByGender is used but not imported; add the import at the top of
the file: import { suffixByGender } from '../../../shared/utils/helpers' and
ensure the import path is correct relative to this file so the suffixByGender
utility is available where used.
| if (user.type === 'staff') { | ||
| const users = await repository.user.list() | ||
| const allStaffExceptUser = users.filter((u) => u.type === 'staff' && u.id !== user.id) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid loading all users; filter in the query layer.
Fetching all users and filtering in memory doesn’t scale. Prefer a repository method that returns only staff and excludes the current user.
I can add a repository.user.listStaffExcept(userId) helper if desired.
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/epic/index.post.ts around lines 33 to 36,
avoid loading all users and filtering in memory; instead call a repository
method that returns only staff excluding the current user (e.g.
repository.user.listStaffExcept(user.id)). Add or use a
repository.user.listStaffExcept(userId) implementation that performs the filter
at the data/query layer, replace the current repository.user.list() + in-memory
filter with an awaited call to that method, and ensure types/returns match
existing usage (handle empty results and errors as before).


Summary by CodeRabbit
New Features
Style