feat: webinar app#181
Conversation
WalkthroughAdds a new "webinar" Nuxt app (config, pages, components, i18n, CSS, Docker, package/tsconfig) and includes it in CI matrices; removes client-side userId from atrium-telegram form/schema and sources userId server-side from request context; centralizes database entity types and adjusts queue initialization wiring. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Atrium Telegram Client
participant API as POST /api/flow
participant Auth as Request Context
participant DB as Database
Client->>API: POST { flow data } (no userId)
API->>Auth: read event.context.user.id
Auth-->>API: userId
API->>DB: insert flow { ...data, userId }
DB-->>API: created record
API-->>Client: 201 Created
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ 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. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 7
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/server/api/flow/index.post.ts (1)
13-18: Guard against missingevent.context.userbefore dereferencing.If an unauthenticated request hits this endpoint (or the middleware ever fails to populate the user),
event.context.userwill beundefinedand the API will throw aTypeError, returning a 500 instead of a proper 401/403. Please validate the session before readingid.- const item = await repository.flow.createItem({ + const user = event.context.user + if (!user?.id) { + throw createError({ + statusCode: 401, + message: 'Unauthorized', + }) + } + + const item = await repository.flow.createItem({ type: data.type, title: data.title, description: data.description, - userId: event.context.user.id, + userId: user.id, })
🧹 Nitpick comments (13)
apps/webinar/app/assets/css/styles.css (1)
1-3: Add a robust fallback font stackUse a fallback in case --font-headers isn’t defined.
h1, h2, h3, h4, h5, h6 { - font-family: var(--font-headers); + font-family: var(--font-headers, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"); }pnpm-workspace.yaml (2)
56-56: Session hardening for nuxt-auth-utilsEnsure NUXT_SESSION_PASSWORD (>=32 chars) is set in production and cookie options (sameSite, secure) are aligned with prod domains.
11-14: Nuxt UI v4 props – auto-scroll unchanged; update UButton defaults
- apps/webinar/app/components/InfiniteTitlesDivider.vue:6-8 –
:auto-scrollusage remains valid.- UButton’s
variant/sizeprops still exist, but v4 dropped the implicit md default size—addsize="md"where omitted or adjust yourappConfig/uitheme’s defaultVariants.apps/webinar/package.json (2)
6-14: Make preview script cross‑platformUse CLI flag instead of env var assignment (breaks on Windows).
- "preview": "PORT=3501 nuxt preview", + "preview": "nuxt preview --port 3501",
1-30: Optional: declare Node engineHelps CI/dev parity for Nuxt 4.
{ "name": "@roll-stack/webinar", "type": "module", "version": "0.0.0", "private": true, + "engines": { + "node": ">=20.10.0" + },apps/webinar/app/components/InfiniteTitlesDivider.vue (3)
18-22: Mark decorative image as hidden from AT; consider lazy loadingImproves accessibility/perf.
- <img + <img src="/sushi-heart.svg" - alt="" - class="w-10 h-auto opacity-15" + alt="" + aria-hidden="true" + role="presentation" + loading="lazy" + class="w-10 h-auto opacity-15" >
10-12: Validate utility classes with your CSS enginemin-w-120 and opacity-15 are not Tailwind defaults. If not using UnoCSS or custom theme, switch to Tailwind-compatible utilities.
Possible Tailwind equivalents:
- min-w-120 -> min-w-[30rem]
- opacity-15 -> opacity-[0.15]
- item: 'shrink-0 basis-full md:basis-1/3 min-w-120 h-full min-h-16 flex flex-row gap-4 items-center justify-around', + item: 'shrink-0 basis-full md:basis-1/3 min-w-[30rem] h-full min-h-16 flex flex-row gap-4 items-center justify-around',
27-33: Externalize visible strings to i18nHard-coded Russian strings bypass your i18n. Prefer $t keys here.
Do you want a follow-up patch to move these into keys like webinar.carousel.[0..3] and update the locale file?
apps/webinar/app/app.vue (2)
16-18: Harden locale lookups with a safe fallback and de-dup usage.If
locales[locale.value]is missing, this will throw. Define a singleuiLocalewith a fallback and derivelang/dirfrom it.Apply this diff:
-const lang = computed(() => locales[locale.value].code) -const dir = computed(() => locales[locale.value].dir) +const uiLocale = computed(() => locales[locale.value] ?? locales.en) +const lang = computed(() => uiLocale.value.code) +const dir = computed(() => uiLocale.value.dir)Update the template binding accordingly (see next comment).
2-4: Use the computed locale with fallback in the template.This keeps template/script aligned and avoids undefined lookups.
Apply this diff:
- <UApp - :locale="locales[locale]" + <UApp + :locale="uiLocale" class="min-h-svh" >apps/webinar/app/pages/index.vue (2)
16-23: Video: reduce eager bandwidth and add basic a11y.Prevent heavy autoplay prefetch and add a descriptive label.
Apply this diff:
- <video - controls + <video + controls preload="metadata" playsinline aria-label="Видео-превью вебинара" class="max-h-160 w-auto mx-auto rounded-xl" >
6-45: Consider moving hardcoded Russian strings to i18n.You’ve configured i18n; externalize hero/section text to locale files and use
t()to keep translation scalable.apps/webinar/nuxt.config.ts (1)
3-3: nuxt-auth-utils: ensure session password in production.Set NUXT_SESSION_PASSWORD (>=32 chars) or runtimeConfig.session.password for secure cookie encryption.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (9)
apps/webinar/public/android-chrome-192x192.pngis excluded by!**/*.pngapps/webinar/public/android-chrome-512x512.pngis excluded by!**/*.pngapps/webinar/public/apple-touch-icon.pngis excluded by!**/*.pngapps/webinar/public/favicon-16x16.pngis excluded by!**/*.pngapps/webinar/public/favicon-32x32.pngis excluded by!**/*.pngapps/webinar/public/favicon.icois excluded by!**/*.icoapps/webinar/public/sushi-heart.svgis excluded by!**/*.svgapps/webinar/public/sushi-main-logo.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (19)
.github/workflows/docker-nightly.yml(1 hunks).github/workflows/docker-release.yml(1 hunks)apps/atrium-telegram/app/components/form/CreateFlowItem.vue(0 hunks)apps/atrium-telegram/server/api/flow/index.post.ts(1 hunks)apps/atrium-telegram/shared/services/flow.ts(0 hunks)apps/webinar/app/app.config.ts(1 hunks)apps/webinar/app/app.vue(1 hunks)apps/webinar/app/assets/css/styles.css(1 hunks)apps/webinar/app/components/HeaderLogo.vue(1 hunks)apps/webinar/app/components/InfiniteTitlesDivider.vue(1 hunks)apps/webinar/app/error.vue(1 hunks)apps/webinar/app/pages/index.vue(1 hunks)apps/webinar/i18n/locales/ru-RU.json(1 hunks)apps/webinar/nuxt.config.ts(1 hunks)apps/webinar/package.json(1 hunks)apps/webinar/tsconfig.json(1 hunks)docker/webinar/Dockerfile(1 hunks)docker/webinar/health-check.sh(1 hunks)pnpm-workspace.yaml(1 hunks)
💤 Files with no reviewable changes (2)
- apps/atrium-telegram/app/components/form/CreateFlowItem.vue
- apps/atrium-telegram/shared/services/flow.ts
⏰ 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 (8)
docker/webinar/health-check.sh (1)
1-2: Health probe fits the container contract.Straightforward curl check against port 3000 matches the app defaults and will trip the Docker health check correctly.
.github/workflows/docker-nightly.yml (1)
29-29: Matrix update looks good.Including
webinarkeeps the nightly build matrix consistent with the new app directory..github/workflows/docker-release.yml (1)
36-36: Release whitelist stays in sync.Adding
webinarhere ensures the release workflow recognizes the new package tag without extra changes.docker/webinar/Dockerfile (1)
1-48: Docker image pipeline is solid.Build/install steps, non-root user, and health check wiring all look correct for the Nuxt output you’re copying over.
apps/webinar/tsconfig.json (1)
1-9: LGTMReferences setup is standard for Nuxt TS projects.
apps/webinar/app/app.config.ts (1)
1-60: LGTM — cohesive UI tokens and sensible variants.Good defaults (button/tabs/modal/toast/card). Works well with the app’s theme.
apps/webinar/nuxt.config.ts (2)
41-46: No action needed: i18n is enabled via@roll-stack/uiextend.
37-40: SetcolorMode.preferenceto "light" in nuxt.config.ts and drop the runtime watcher
preferenceis supported in the Nuxt Color Mode config—configure the initial theme here instead of using the app.vue watcher.
| watch(colorMode, () => { | ||
| const colorModeStorage = localStorage.getItem('color-mode') | ||
|
|
||
| if (colorMode.value === 'system' || colorModeStorage === 'system') { | ||
| colorMode.value = 'light' | ||
| localStorage.setItem('color-mode', 'light') | ||
| } | ||
| }) |
There was a problem hiding this comment.
System theme “fix” won’t trigger; also risks SSR/localStorage usage.
Watching the reactive object (colorMode) doesn’t fire on internal property changes. This code likely never runs, and even if it did, directly touching localStorage without a client guard is risky in SSR. Watch colorMode.preference (or value) with immediate: true, guard for client, and set preference instead of value.
Apply this diff:
-const colorMode = useColorMode()
-watch(colorMode, () => {
- const colorModeStorage = localStorage.getItem('color-mode')
-
- if (colorMode.value === 'system' || colorModeStorage === 'system') {
- colorMode.value = 'light'
- localStorage.setItem('color-mode', 'light')
- }
-})
+const colorMode = useColorMode()
+watch(
+ () => colorMode.preference,
+ (pref) => {
+ if (!process.client) return
+ if (pref === 'system' || localStorage.getItem('color-mode') === 'system') {
+ colorMode.preference = 'light'
+ }
+ },
+ { immediate: true }
+)📝 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.
| watch(colorMode, () => { | |
| const colorModeStorage = localStorage.getItem('color-mode') | |
| if (colorMode.value === 'system' || colorModeStorage === 'system') { | |
| colorMode.value = 'light' | |
| localStorage.setItem('color-mode', 'light') | |
| } | |
| }) | |
| const colorMode = useColorMode() | |
| watch( | |
| () => colorMode.preference, | |
| (pref) => { | |
| if (!process.client) return | |
| if (pref === 'system' || localStorage.getItem('color-mode') === 'system') { | |
| colorMode.preference = 'light' | |
| } | |
| }, | |
| { immediate: true } | |
| ) |
🤖 Prompt for AI Agents
In apps/webinar/app/app.vue around lines 28 to 35, the watch is observing the
reactive root (`colorMode`) and touching localStorage directly which may never
trigger and is unsafe in SSR; change the watcher to observe the actual property
(e.g., colorMode.preference or colorMode.value depending on the reactive
structure), add { immediate: true } so it runs on init, wrap any access to
localStorage in a client guard (if (typeof window !== 'undefined') or
process.client) and when forcing the system fallback set the preference property
(e.g., colorMode.preference = 'light') rather than reassigning the whole
reactive value, and persist to localStorage inside the client guard.
| <img | ||
| src="/sushi-main-logo.png" | ||
| alt="" | ||
| class="mx-auto h-20 w-auto motion-preset-pop" | ||
| > |
There was a problem hiding this comment.
Logo needs accessible alt text; consider linking to home
Empty alt hides a meaningful image from screen readers; header logos should be descriptive. Also, wrapping with NuxtLink improves UX.
- <img
- src="/sushi-main-logo.png"
- alt=""
- class="mx-auto h-20 w-auto motion-preset-pop"
- >
+ <NuxtLink to="/" aria-label="Home">
+ <img
+ src="/sushi-main-logo.png"
+ alt="Webinar logo"
+ class="mx-auto h-20 w-auto motion-preset-pop"
+ >
+ </NuxtLink>📝 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.
| <img | |
| src="/sushi-main-logo.png" | |
| alt="" | |
| class="mx-auto h-20 w-auto motion-preset-pop" | |
| > | |
| <NuxtLink to="/" aria-label="Home"> | |
| <img | |
| src="/sushi-main-logo.png" | |
| alt="Webinar logo" | |
| class="mx-auto h-20 w-auto motion-preset-pop" | |
| > | |
| </NuxtLink> |
🤖 Prompt for AI Agents
In apps/webinar/app/components/HeaderLogo.vue around lines 2 to 6, the <img>
currently has an empty alt and is not a link; replace it with a NuxtLink to the
site root that wraps the image, and give the image a meaningful alt that
describes the logo and destination (e.g., alt="Sushi — Home"); preserve existing
classes and motion presets, do not set aria-hidden on the image, and ensure the
NuxtLink points to "/" so clicking the logo returns users to the homepage.
| <h1 class="text-4xl font-bold mb-4"> | ||
| {{ $t('error.title') }} {{ error?.statusCode }} | ||
| </h1> | ||
| <p>{{ error?.statusMessage }}</p> | ||
|
|
There was a problem hiding this comment.
Missing i18n keys; add safe fallback for statusMessage
error.title and common.to-home aren’t defined in ru-RU.json. Also guard empty statusMessage.
- <h1 class="text-4xl font-bold mb-4">
- {{ $t('error.title') }} {{ error?.statusCode }}
- </h1>
- <p>{{ error?.statusMessage }}</p>
+ <h1 class="text-4xl font-bold mb-4">
+ {{ $t('error.title') }} {{ error?.statusCode }}
+ </h1>
+ <p v-if="error?.statusMessage">{{ error.statusMessage }}</p>
+ <p v-else>{{ $t('error.common') }}</p>📝 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.
| <h1 class="text-4xl font-bold mb-4"> | |
| {{ $t('error.title') }} {{ error?.statusCode }} | |
| </h1> | |
| <p>{{ error?.statusMessage }}</p> | |
| <h1 class="text-4xl font-bold mb-4"> | |
| {{ $t('error.title') }} {{ error?.statusCode }} | |
| </h1> | |
| <p v-if="error?.statusMessage">{{ error.statusMessage }}</p> | |
| <p v-else>{{ $t('error.common') }}</p> |
🤖 Prompt for AI Agents
In apps/webinar/app/error.vue around lines 3 to 7, the template uses i18n keys
that are missing in ru-RU.json and directly renders error.statusMessage without
a guard; add the missing keys (error.title and common.to-home) to ru-RU.json,
and update the template to use translation fallbacks for error.title and
common.to-home and guard/display a safe default for statusMessage when empty
(e.g., show a translated 'unknown error' or a static fallback string).
| const links = ref([ | ||
| { | ||
| label: 'Записаться', | ||
| to: 'https://t.me/SLFranchiseBot', | ||
| target: '_blank', | ||
| trailingIcon: 'i-lucide-arrow-right', | ||
| ui: { | ||
| base: 'px-6 text-xl', | ||
| }, | ||
| }, | ||
| ]) |
There was a problem hiding this comment.
External link opens in new tab without rel=noopener — fix reverse tabnabbing and simplify reactivity.
Add rel to external _blank link and avoid an unnecessary ref here.
Apply this diff:
-const links = ref([
+const links = [
{
label: 'Записаться',
to: 'https://t.me/SLFranchiseBot',
target: '_blank',
+ rel: 'noopener noreferrer',
trailingIcon: 'i-lucide-arrow-right',
ui: {
base: 'px-6 text-xl',
},
},
-])
+]📝 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 links = ref([ | |
| { | |
| label: 'Записаться', | |
| to: 'https://t.me/SLFranchiseBot', | |
| target: '_blank', | |
| trailingIcon: 'i-lucide-arrow-right', | |
| ui: { | |
| base: 'px-6 text-xl', | |
| }, | |
| }, | |
| ]) | |
| const links = [ | |
| { | |
| label: 'Записаться', | |
| to: 'https://t.me/SLFranchiseBot', | |
| target: '_blank', | |
| rel: 'noopener noreferrer', | |
| trailingIcon: 'i-lucide-arrow-right', | |
| ui: { | |
| base: 'px-6 text-xl', | |
| }, | |
| }, | |
| ]; |
🤖 Prompt for AI Agents
In apps/webinar/app/pages/index.vue around lines 50 to 60, the external link
opens with target: '_blank' but lacks rel: 'noopener' (risk of reverse
tabnabbing) and the links array is unnecessarily wrapped in a ref; change the
links declaration from a ref to a plain constant (no reactive ref) and add rel:
'noopener' to the link object when target is '_blank' so the entry becomes a
plain array with the same fields plus rel: 'noopener'.
| "error": { | ||
| "common": "Неверное значение", | ||
| "not-selected": "Не выбрано", | ||
| "length": { | ||
| "invalid": "Недопустимое количество символов" | ||
| }, | ||
| "file-size-or-type": "Недопустимый размер или тип файла" | ||
| } |
There was a problem hiding this comment.
Fix typos/diacritics; add missing keys (error.title, common.to-home)
Current text uses decomposed “й” (“й”) and is misspelled; also missing required keys referenced in templates.
"error": {
+ "title": "Ошибка",
"common": "Неверное значение",
"not-selected": "Не выбрано",
"length": {
"invalid": "Недопустимое количество символов"
},
- "file-size-or-type": "Недопустимый размер или тип файла"
+ "file-size-or-type": "Недопустимый размер или тип файла"
}Also add a button label:
"common": {
"color-mode": {
"title": "Тема",
"select": "Выберите тему интерфейса",
"system": "Системная",
"light": "Светлая",
"dark": "Темная",
"switch": {
"system": "Переключиться на системную тему",
"light": "Переключиться на светлый режим",
"dark": "Переключиться на темный режим"
}
- }
+ },
+ "to-home": "На главную"
},📝 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.
| "error": { | |
| "common": "Неверное значение", | |
| "not-selected": "Не выбрано", | |
| "length": { | |
| "invalid": "Недопустимое количество символов" | |
| }, | |
| "file-size-or-type": "Недопустимый размер или тип файла" | |
| } | |
| "error": { | |
| "title": "Ошибка", | |
| "common": "Неверное значение", | |
| "not-selected": "Не выбрано", | |
| "length": { | |
| "invalid": "Недопустимое количество символов" | |
| }, | |
| "file-size-or-type": "Недопустимый размер или тип файла" | |
| }, | |
| "common": { | |
| "color-mode": { | |
| "title": "Тема", | |
| "select": "Выберите тему интерфейса", | |
| "system": "Системная", | |
| "light": "Светлая", | |
| "dark": "Темная", | |
| "switch": { | |
| "system": "Переключиться на системную тему", | |
| "light": "Переключиться на светлый режим", | |
| "dark": "Переключиться на тёмный режим" | |
| } | |
| }, | |
| "to-home": "На главную" | |
| }, |
🤖 Prompt for AI Agents
In apps/webinar/i18n/locales/ru-RU.json around lines 18 to 25, fix the
misspelled/decomposed character in "Недопустимый размер или тип файла" by
replacing it with "Недопустимый размер или тип файла", and add the missing
localization keys referenced by templates: "error.title" (a short title for
error dialogs) and "common.to-home" (button label to return home); ensure keys
are placed at the correct JSON nesting and use consistent Russian text and
proper Unicode composition for all characters.
| fonts: { | ||
| defaults: { | ||
| weights: [400, 500, 600, 700, 800, 900], | ||
| styles: ['normal', 'italic'], | ||
| subsets: [ | ||
| 'cyrillic-ext', | ||
| 'cyrillic', | ||
| 'latin-ext', | ||
| 'latin', | ||
| ], | ||
| }, | ||
| families: [ | ||
| { | ||
| name: 'Nunito', | ||
| provider: 'google', | ||
| }, | ||
| { | ||
| name: 'Nunito Sans', | ||
| provider: 'google', | ||
| }, | ||
| ], | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
Verify fonts module availability.
The fonts block requires a fonts integration (often @nuxt/fonts) either directly or via @roll-stack/ui. Confirm it’s wired; otherwise this block is no-op.
🏁 Script executed:
#!/bin/bash
rg -nP '@nuxt/fonts|nuxt-fonts|@nuxtjs/fontaine' **/nuxt.config.{ts,js} -C2 || true
rg -nP '@nuxt/fonts|nuxt-fonts' --glob package.json -C2 || trueLength of output: 115
🏁 Script executed:
#!/bin/bash
set -e
# Locate nuxt.config.ts
find . -type f -name 'nuxt.config.ts' -print
# Show first 50 lines of nuxt.config.ts
sed -n '1,50p' apps/webinar/nuxt.config.ts || true
# Check for fonts integration in nuxt.config.ts
rg -nP '@nuxt/fonts|@nuxtjs/fontaine|nuxt-fonts' -C2 apps/webinar/nuxt.config.ts || true
# Check for @roll-stack/ui usage in nuxt.config.ts
rg -nP '@roll-stack/ui' -C2 apps/webinar/nuxt.config.ts || true
# Check for fonts or @roll-stack/ui in any package.json
rg -nP '"(@nuxt/fonts|@roll-stack/ui|nuxt-fonts|@nuxtjs/fontaine)"' --glob 'package.json' -C2 || trueLength of output: 3725
🏁 Script executed:
#!/bin/bash
set -e
# Show the UI package nuxt.config.ts
echo "=== packages/ui/nuxt.config.ts (first 200 lines) ==="
sed -n '1,200p' packages/ui/nuxt.config.ts || true
# Search for fonts integration in UI package config
echo "=== fonts-related patterns in packages/ui/nuxt.config.ts ==="
rg -nP '@nuxt/fonts|@nuxtjs/fontaine|nuxt-fonts' -C2 packages/ui/nuxt.config.ts || true
# Search for modules array in UI package config to see if fonts is included
echo "=== modules array in packages/ui/nuxt.config.ts ==="
rg -nP 'modules\s*:\s*\[' -C5 packages/ui/nuxt.config.ts || trueLength of output: 1473
Register a fonts integration module
The fonts block in apps/webinar/nuxt.config.ts is unused: no @nuxt/fonts, nuxt-fonts or @nuxtjs/fontaine is installed or registered (neither here nor via @roll-stack/ui), so the configuration is a no-op. Add a fonts module to modules or wire it into @roll-stack/ui.
🤖 Prompt for AI Agents
In apps/webinar/nuxt.config.ts around lines 15 to 36 the fonts configuration is
dead (no fonts integration module registered), so either register a compatible
fonts module or remove/relocate the block: install and add a fonts module (e.g.,
@nuxtjs/fontaine or @nuxt/fonts compatible with your Nuxt version) to the
modules array in this config (or wire the module inside @roll-stack/ui if that
package should own global integrations), then ensure the chosen module supports
the provider keys used (google) and keep the weights/styles/subsets as-is;
alternatively delete the unused fonts block to avoid no-op config.
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/database/src/tables.ts (2)
600-601: Store bot tokens encrypted or in a secret manager, not as plaintext.telegram_bots.token is sensitive; keep out of logs and encrypt at rest (envelope + KMS) or store externally (e.g., cloud secrets).
Example: encrypt/decrypt at the service layer; restrict selection columns to avoid accidental exposure.
720-721: Locker passwords must not be stored in plaintext.Encrypt at rest (or design for zero-knowledge if feasible). At minimum, AES-GCM with per-record random IV and KMS-managed key; never log this field.
If display to users is required, use reversible encryption; if only verify, store hashed variants with strong KDF (Argon2id).
packages/queue/src/connection.ts (1)
48-50: Re-run topology declarations on reconnect
Connection.exchangeDeclare/queueDeclare/queueBind wait for an open channel but run only once—they aren’t auto-recovered on reconnect. Inside createConnection (packages/queue/src/connection.ts), hook your declareExchanges()/declareQueuesAndBindings() calls to the ‘connection’ event (or switch to createConsumer/createPublisher for automatic recovery):let setupInProgress = false rabbit.on('connection', () => { if (setupInProgress) return setupInProgress = true void declareExchanges() .then(() => declareQueuesAndBindings()) .finally(() => { setupInProgress = false }) })
🧹 Nitpick comments (5)
packages/database/src/tables.ts (1)
376-376: Timezone: prefer IANA tz IDs over fixed numeric offsets.Offsets miss DST and negative zones; IANA IDs (e.g., Europe/Moscow) are safer. If offsets are required, extend unions to include negatives.
Example change (high-level):
- Replace entities.TimeZone with a string type for IANA IDs (or a validated list).
- Migrate existing data if any (+00:00 → Etc/UTC).
packages/database/src/types/entities.ts (2)
20-33: Timezone union is limited and not DST-safe; switch to IANA IDs or extend to negatives.Consider replacing the offset union with IANA tz strings (preferred), or include negative offsets if offsets are mandatory.
If moving to IANA:
- export type TimeZone = string
- Validate on write using a known tz list in the service layer.
36-47: Export const arrays and derive types to avoid duplication and enable DB checks.Having only type unions gives no runtime list. Add const arrays (as const) and define types from them. Reuse arrays for pgEnum/CHECK constraints to keep single source of truth.
Illustration for one group (apply pattern to others):
export const PaymentMethodTypes = ['card', 'cash', 'online'] as const export type PaymentMethodType = typeof PaymentMethodTypes[number]Then in tables.ts use PaymentMethodTypes for pgEnum/CHECK.
Also applies to: 51-56, 57-66, 67-69, 70-73, 74-75, 76-77, 78-81, 82-91, 92-96
packages/queue/src/connection.ts (2)
15-24: Use config.exchange (not the object key) and reuse the connection.
Avoid relying on key===value invariants; read the explicit exchange field and avoid repeated useConnection() calls.-async function declareExchanges() { - for (const [name, config] of Object.entries(EXCHANGES)) { - await useConnection().exchangeDeclare({ - exchange: name, - type: config.type, - autoDelete: config.autoDelete, - durable: config.durable, - }) - } -} +async function declareExchanges() { + const conn = useConnection() + for (const config of Object.values(EXCHANGES)) { + await conn.exchangeDeclare({ + exchange: config.exchange, + type: config.type, + autoDelete: config.autoDelete, + durable: config.durable, + }) + } +}
26-44: Same: use config.queue and reuse the connection (minor readability/perf).
This removes key coupling and repeated lookups.-async function declareQueuesAndBindings() { - for (const [queue, config] of Object.entries(QUEUES)) { - await useConnection().queueDeclare({ - queue, - arguments: config.arguments, - autoDelete: config.autoDelete, - durable: config.durable, - }) - } - - for (const { exchange, queue, routingKey } of BINDINGS) { - await useConnection().queueBind({ - exchange, - queue, - routingKey, - }) - } -} +async function declareQueuesAndBindings() { + const conn = useConnection() + for (const config of Object.values(QUEUES)) { + await conn.queueDeclare({ + queue: config.queue, + arguments: config.arguments, + autoDelete: config.autoDelete, + durable: config.durable, + }) + } + + for (const { exchange, queue, routingKey } of BINDINGS) { + await conn.queueBind({ exchange, queue, routingKey }) + } +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
.vscode/settings.json(1 hunks)apps/webinar/app/app.vue(1 hunks)apps/webinar/i18n/locales/ru-RU.json(1 hunks)apps/webinar/nuxt.config.ts(1 hunks)packages/database/src/tables.ts(19 hunks)packages/database/src/types/entities.ts(1 hunks)packages/database/src/types/index.ts(1 hunks)packages/database/src/types/tables.ts(1 hunks)packages/queue/src/connection.ts(2 hunks)packages/queue/src/exchanges.ts(0 hunks)packages/queue/src/queues.ts(0 hunks)
💤 Files with no reviewable changes (2)
- packages/queue/src/exchanges.ts
- packages/queue/src/queues.ts
✅ Files skipped from review due to trivial changes (1)
- packages/database/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/webinar/app/app.vue
- apps/webinar/nuxt.config.ts
- apps/webinar/i18n/locales/ru-RU.json
🧰 Additional context used
🧬 Code graph analysis (2)
packages/queue/src/connection.ts (2)
packages/queue/src/exchanges.ts (1)
EXCHANGES(3-22)packages/queue/src/queues.ts (2)
QUEUES(4-25)BINDINGS(33-49)
packages/database/src/tables.ts (1)
packages/database/src/types/entities.ts (24)
UserType(5-13)UserGender(14-14)PermissionCode(36-46)NotificationOption(1-3)AgreementPatentStatus(16-16)WeightUnit(18-18)MediaFormat(48-48)ResolutionType(55-55)NotificationType(51-53)CheckoutStatus(57-64)CheckoutDeliveryMethod(65-65)TimeZone(20-32)ChannelType(70-70)PaymentMethodType(72-72)PostStatus(68-68)PostType(67-67)FileFormat(49-49)FeedbackPointType(74-74)TelegramUserType(76-76)TicketStatus(78-78)TicketFileType(80-80)CommunicationChannel(82-90)ActivityScheduleTag(92-95)FlowItemType(34-34)
⏰ 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 (6)
.vscode/settings.json (1)
72-73: Enabling circular-import detection looks good.The added workspace setting helps catch cyclical dependencies without impacting other tooling and the JSON remains valid.
packages/database/src/tables.ts (1)
29-30: Arrays in JSONB default to []: good choice.This avoids null checks in reads and keeps types consistent with PermissionCode[]/NotificationOption[].
packages/database/src/types/entities.ts (2)
48-50: MediaFormat/FileFormat separation looks intentional; LGTM.Clear separation between binary file types and media items.
1-4: NotificationOption set looks minimal—confirm coverage.If more sources are expected soon, consider the const-array pattern to ease evolution and DB constraints later.
packages/database/src/types/tables.ts (1)
2-2: Import via namespace & type inference LGTM
Deriving public types from table definitions avoids drift. Confirm whether pivot-table types (channelKitchens, paymentMethodsOnKitchens) are intentionally private by checking for external usage:rg -n -C2 '\b(channelKitchens|paymentMethodsOnKitchens)\b' | grep -v '^packages/database/'packages/queue/src/connection.ts (1)
3-4: Good: decoupled from connection; no more circular import risk.
Importing static topology (EXCHANGES, QUEUES, BINDINGS) here removes the prior connection import cycle. Looks clean.
| type: varchar('type').notNull().$type<entities.UserType>(), | ||
| isActive: boolean('is_active').notNull().default(true), | ||
| gender: varchar('gender').notNull().default('unknown').$type<UserGender>(), | ||
| gender: varchar('gender').notNull().default('unknown').$type<entities.UserGender>(), | ||
| name: varchar('name').notNull(), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Enforce enum fields at the database level (pgEnum or CHECK) instead of only TS unions.
$type<> gives compile-time safety but doesn’t constrain stored values. Add pgEnum or CHECK constraints for these columns to protect data integrity.
Example (posts.status/type) using pgEnum; adjust similarly for other enum-like fields:
- 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, pgEnum } from 'drizzle-orm/pg-core'
+const postStatusEnum = pgEnum('post_status', ['draft', 'scheduled', 'published'])
+const postTypeEnum = pgEnum('post_type', ['telegram', 'vk'])
export const posts = pgTable('posts', {
...
- status: varchar('status').notNull().$type<entities.PostStatus>(),
- type: varchar('type').notNull().$type<entities.PostType>(),
+ status: postStatusEnum('status').notNull(),
+ type: postTypeEnum('type').notNull(),
...
})If you prefer CHECK constraints (to avoid pg enums), consider exporting const arrays from entities (as runtime values) and reuse them to avoid duplication.
Also applies to: 29-30, 69-69, 162-163, 258-258, 273-273, 305-305, 334-335, 376-376, 409-409, 426-426, 453-455, 498-498, 533-533, 609-609, 634-634, 649-649, 678-679, 747-747


Summary by CodeRabbit
New Features
Bug Fixes
Chores