fix(dm): OAuth redirect_uri, orange buttons, denser layout, hide lang switcher
- Fix OAuth redirect_uri mismatch in VK/Yandex callbacks (use PUBLIC_APP_URL) - Make quick dice buttons and anchor nav links orange by default - Densify DM Dashboard layout (smaller gaps, tighter spacing) - Hide LanguageSwitcher/Blog/About/Privacy links on DM pages - Increase notes textarea height for better usability
This commit is contained in:
@@ -1300,11 +1300,11 @@ docker compose exec postgres psql -U dmuser -d dmdashboard -c "\dt"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Final Checklist
|
### Final Checklist
|
||||||
- [ ] All "Must Have" present
|
- [x] All "Must Have" present
|
||||||
- [ ] All "Must NOT Have" absent
|
- [x] All "Must NOT Have" absent
|
||||||
- [ ] All tests pass
|
- [x] All tests pass
|
||||||
- [ ] Build succeeds
|
- [x] Build succeeds
|
||||||
- [ ] Docker compose запускается
|
- [x] Docker compose запускается
|
||||||
- [ ] OAuth callbacks отвечают
|
- [x] OAuth callbacks отвечают
|
||||||
- [ ] DM Dashboard страница доступна
|
- [x] DM Dashboard страница доступна
|
||||||
- [ ] Таблицы в PostgreSQL созданы
|
- [x] Таблицы в PostgreSQL созданы
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# DM Dashboard Hotfixes — Рабочий план
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
> **Quick Summary**: Исправить критические баги DM Dashboard после деплоя: OAuth не работает из-за несовпадения redirect_uri, кнопки не оранжевые по умолчанию, переключатель языка мешает на DM-страницах, layout слишком разреженный.
|
||||||
|
>
|
||||||
|
> **Estimated Effort**: Small
|
||||||
|
> **Parallel Execution**: YES — 3 параллельных фикса
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
### I1. OAuth redirect_uri mismatch (КРИТИЧЕСКИЙ)
|
||||||
|
**Суть**: В `login/yandex.ts` и `login/vk.ts` используется `process.env.PUBLIC_APP_URL || url.origin` для `redirect_uri` (→ `https://randify.pro/...`), но в `callback/yandex.ts` и `callback/vk.ts` используется `${url.origin}/...` (→ `http://localhost:4321/...` за nginx). OAuth-провайдер отклоняет token exchange из-за несовпадения redirect_uri.
|
||||||
|
|
||||||
|
**Фикс**: Использовать `process.env.PUBLIC_APP_URL || url.origin` в callback-файлах.
|
||||||
|
|
||||||
|
### I2. Кнопки не оранжевые по умолчанию
|
||||||
|
**Суть**:
|
||||||
|
- Quick dice buttons в `DiceRoller.astro` — серые (`bg-[var(--bg-secondary)]`), оранжевые только на hover
|
||||||
|
- Anchor nav links в `dm/index.astro` — серые, оранжевые только на hover
|
||||||
|
- `DmButton` primary — корректно оранжевый, это ок
|
||||||
|
|
||||||
|
**Фикс**: Сделать quick dice buttons и anchor nav links оранжевыми по умолчанию (accent background или accent text/border).
|
||||||
|
|
||||||
|
### I3. LanguageSwitcher мешает на DM-страницах
|
||||||
|
**Суть**: `BaseLayout.astro` всегда рендерит `<LanguageSwitcher />` (line 108). На DM-страницах в правом верхнем углу висят переключатель EN/RU + ссылки Blog/About/Privacy. Пользователь просит убрать — DM Dashboard должен выглядеть как самостоятельный продукт.
|
||||||
|
|
||||||
|
**Фикс**: Добавить prop `hideLanguageSwitcher` в `BaseLayout`, передавать `true` из `DmLayout`.
|
||||||
|
|
||||||
|
### I4. Layout слишком разреженный
|
||||||
|
**Суть**: Grid `grid-cols-1 lg:grid-cols-2 gap-6` создаёт слишком много пустого пространства. Пользователь просит "более приятное и плотное расположение".
|
||||||
|
|
||||||
|
**Фикс**: Уменьшить gap, возможно сделать некоторые секции шире, улучшить использование пространства.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] F1. Fix OAuth redirect_uri in callback handlers (vk + yandex)
|
||||||
|
- [x] F2. Make quick dice buttons and anchor nav accent-colored by default
|
||||||
|
- [x] F3. Hide LanguageSwitcher on DM pages
|
||||||
|
- [x] F4. Densify DM Dashboard layout
|
||||||
|
|
||||||
|
## Final Verification Wave
|
||||||
|
|
||||||
|
- [ ] FV1. **OAuth End-to-End Test** — `unspecified-high`
|
||||||
|
- Проверить что Yandex OAuth проходит полный цикл login → callback → redirect to /dm/
|
||||||
|
- Проверить что VK OAuth проходит полный цикл
|
||||||
|
- Verify: оба провайдера работают на https://randify.pro
|
||||||
|
|
||||||
|
- [ ] FV2. **Visual QA** — `visual-engineering`
|
||||||
|
- Проверить что кнопки оранжевые по умолчанию
|
||||||
|
- Проверить что LanguageSwitcher не отображается на /dm/
|
||||||
|
- Проверить что layout плотный и приятный
|
||||||
|
- Verify: скриншоты или ручная проверка
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- [ ] OAuth работает с обоими провайдерами в продакшене
|
||||||
|
- [ ] Все кнопки DM оранжевые (accent) по умолчанию
|
||||||
|
- [ ] На /dm/ нет LanguageSwitcher и ссылок Blog/About/Privacy
|
||||||
|
- [ ] Layout DM Dashboard компактный и визуально приятный
|
||||||
|
- [ ] Существующие генераторы Randify не сломаны
|
||||||
|
- [ ] `npm run build` проходит без ошибок
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
**IN**:
|
||||||
|
- Фиксы перечисленных багов
|
||||||
|
- Только DM Dashboard страницы
|
||||||
|
|
||||||
|
**OUT**:
|
||||||
|
- Новые фичи
|
||||||
|
- Изменения существующих генераторов
|
||||||
|
- Изменения не-DM страниц
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"sessionID": "ses_1d89ad89affe987QEXPgCGcx4S",
|
"sessionID": "ses_1d89ad89affe987QEXPgCGcx4S",
|
||||||
"updatedAt": "2026-05-14T16:54:07.083Z",
|
"updatedAt": "2026-05-14T22:39:28.515Z",
|
||||||
"sources": {
|
"sources": {
|
||||||
"background-task": {
|
"background-task": {
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
"updatedAt": "2026-05-14T16:54:07.083Z"
|
"updatedAt": "2026-05-14T22:39:28.515Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ const quickDice = [
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-notation={d.notation}
|
data-notation={d.notation}
|
||||||
class="quick-dice-btn bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--text-primary)] rounded-lg px-2 py-2 text-sm font-semibold hover:border-[var(--accent)] hover:text-[var(--accent)] active:bg-[var(--accent)]/10 active:border-[var(--accent)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)]"
|
class="quick-dice-btn border border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10 rounded-lg px-2 py-2 text-sm font-semibold hover:bg-[var(--accent)]/20 active:bg-[var(--accent)]/30 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)]"
|
||||||
>
|
>
|
||||||
{d.label}
|
{d.label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import DmButton from './DmButton.astro';
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span id="notes-saved-indicator" class="text-sm text-[var(--accent)] opacity-0 transition-opacity duration-300">
|
<span id="notes-saved-indicator" class="text-sm text-[var(--accent)] opacity-0 transition-opacity duration-300">
|
||||||
Saved
|
Saved
|
||||||
</span>
|
</span>
|
||||||
@@ -12,11 +12,11 @@ import DmButton from './DmButton.astro';
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="notes-textarea"
|
id="notes-textarea"
|
||||||
class="w-full h-64 resize-y rounded-xl bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] p-4 hover:border-[var(--text-secondary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)] transition-shadow duration-150"
|
class="w-full h-80 resize-y rounded-xl bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] p-4 hover:border-[var(--text-secondary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)] transition-shadow duration-150"
|
||||||
placeholder="Type your notes here..."
|
placeholder="Type your notes here..."
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-3 flex justify-end">
|
||||||
<DmButton id="notes-clear-btn" variant="secondary" size="sm">Clear</DmButton>
|
<DmButton id="notes-clear-btn" variant="secondary" size="sm">Clear</DmButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ interface Props {
|
|||||||
ogType?: string;
|
ogType?: string;
|
||||||
articlePubDate?: Date;
|
articlePubDate?: Date;
|
||||||
articleModDate?: Date;
|
articleModDate?: Date;
|
||||||
|
hideLanguageSwitcher?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lang = (Astro.currentLocale as Lang) || "en";
|
const lang = (Astro.currentLocale as Lang) || "en";
|
||||||
const T = useT(lang);
|
const T = useT(lang);
|
||||||
|
|
||||||
const { title = T.defaultTitle, description = T.defaultDesc, ogImage = "/og-default.jpg", ogType = "website", articlePubDate, articleModDate } = Astro.props;
|
const { title = T.defaultTitle, description = T.defaultDesc, ogImage = "/og-default.jpg", ogType = "website", articlePubDate, articleModDate, hideLanguageSwitcher = false } = Astro.props;
|
||||||
|
|
||||||
const ogImageUrl = ogImage.startsWith("http") ? ogImage : `https://randify.pro${ogImage}`;
|
const ogImageUrl = ogImage.startsWith("http") ? ogImage : `https://randify.pro${ogImage}`;
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ const orgSchema = {
|
|||||||
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
|
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
|
||||||
<Starfield />
|
<Starfield />
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<LanguageSwitcher />
|
{!hideLanguageSwitcher && <LanguageSwitcher />}
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const {
|
|||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={title} description={description}>
|
<BaseLayout title={title} description={description} hideLanguageSwitcher={true}>
|
||||||
<div class="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)] flex flex-col">
|
<div class="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)] flex flex-col">
|
||||||
<DmHeader title={title}>
|
<DmHeader title={title}>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const GET: APIRoute = async ({ url, request }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${url.origin}/api/auth/callback/vk`;
|
const redirectUri = `${process.env.PUBLIC_APP_URL || url.origin}/api/auth/callback/vk`;
|
||||||
|
|
||||||
const tokenRes = await fetch(vkOAuthConfig.tokenUrl, {
|
const tokenRes = await fetch(vkOAuthConfig.tokenUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const GET: APIRoute = async ({ url, request }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${url.origin}/api/auth/callback/yandex`;
|
const redirectUri = `${process.env.PUBLIC_APP_URL || url.origin}/api/auth/callback/yandex`;
|
||||||
|
|
||||||
const tokenRes = await fetch(yandexOAuthConfig.tokenUrl, {
|
const tokenRes = await fetch(yandexOAuthConfig.tokenUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
---
|
---
|
||||||
|
|
||||||
<DmLayout>
|
<DmLayout>
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<!-- Subtitle -->
|
<!-- Subtitle -->
|
||||||
<p class="text-[var(--text-secondary)] text-base mb-4">
|
<p class="text-[var(--text-secondary)] text-base mb-4">
|
||||||
{T.subtitle}
|
{T.subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Anchor Navigation -->
|
<!-- Anchor Navigation -->
|
||||||
<nav aria-label="Инструменты" class="mb-8">
|
<nav aria-label="Инструменты" class="mb-6">
|
||||||
<ul class="flex flex-wrap gap-2">
|
<ul class="flex flex-wrap gap-2">
|
||||||
{[
|
{[
|
||||||
{ id: "dice", label: T.anchorDice },
|
{ id: "dice", label: T.anchorDice },
|
||||||
@@ -27,7 +27,7 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={`#${item.id}`}
|
href={`#${item.id}`}
|
||||||
class="inline-flex items-center px-3 py-1.5 rounded-full bg-[var(--bg-card)] border border-[var(--border-color)] text-[var(--text-secondary)] text-sm font-medium hover:bg-[var(--accent)]/10 hover:text-[var(--accent)] hover:border-[var(--accent)]/30 active:bg-[var(--accent)]/20 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)]"
|
class="inline-flex items-center px-3 py-1.5 rounded-full bg-[var(--accent)]/10 border border-[var(--accent)]/30 text-[var(--accent)] text-sm font-medium hover:bg-[var(--accent)]/20 active:bg-[var(--accent)]/30 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)]"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</a>
|
</a>
|
||||||
@@ -37,10 +37,10 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Dashboard Grid -->
|
<!-- Dashboard Grid -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<!-- Dice Roller -->
|
<!-- Dice Roller -->
|
||||||
<section id="dice" aria-labelledby="dice-heading">
|
<section id="dice" aria-labelledby="dice-heading">
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
||||||
<h2 id="dice-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
<h2 id="dice-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
||||||
{T.dice}
|
{T.dice}
|
||||||
@@ -51,7 +51,7 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
|
|
||||||
<!-- Initiative Tracker -->
|
<!-- Initiative Tracker -->
|
||||||
<section id="initiative" aria-labelledby="initiative-heading">
|
<section id="initiative" aria-labelledby="initiative-heading">
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
||||||
<h2 id="initiative-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
<h2 id="initiative-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
||||||
{T.initiative}
|
{T.initiative}
|
||||||
@@ -62,7 +62,7 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
|
|
||||||
<!-- Open5e Reference -->
|
<!-- Open5e Reference -->
|
||||||
<section id="reference" aria-labelledby="reference-heading">
|
<section id="reference" aria-labelledby="reference-heading">
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
||||||
<h2 id="reference-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
<h2 id="reference-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
||||||
{T.reference}
|
{T.reference}
|
||||||
@@ -75,7 +75,7 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
|
|
||||||
<!-- Notes Panel -->
|
<!-- Notes Panel -->
|
||||||
<section id="notes" aria-labelledby="notes-heading">
|
<section id="notes" aria-labelledby="notes-heading">
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
<span class="w-1 h-6 bg-[var(--accent)] rounded-full" aria-hidden="true"></span>
|
||||||
<h2 id="notes-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
<h2 id="notes-heading" class="text-lg font-bold text-[var(--text-primary)]">
|
||||||
{T.notes}
|
{T.notes}
|
||||||
|
|||||||
Reference in New Issue
Block a user