From fda6ed48abc415e65b7f13960b528616ee1e9e6f Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:55:51 +0300 Subject: [PATCH] feat: Android download page, analysis retry and a collapsible search - /android is a public page with the latest APK, the server address to copy and three install steps; linked from the sign-in screen and the profile menu - failed audio analysis can be retried from the library page, which also shows the most common error; the analyzer no longer burns attempts on files that are missing because the disk is not mounted - the top bar search folds into a round button and slides open on click, "/" or Ctrl+K Co-Authored-By: Claude Opus 5.5 --- api/openapi.yaml | 26 +- apps/web/src/app/router.tsx | 5 + apps/web/src/pages/AppDownloadPage.tsx | 70 +++ apps/web/src/pages/LoginPage.tsx | 10 +- apps/web/src/pages/admin/LibraryPage.tsx | 21 +- apps/web/src/pages/admin/library.module.css | 14 + apps/web/src/pages/auth.module.css | 77 +++ apps/web/src/shared/ui/icons.ts | 2 + apps/web/src/widgets/AppShell.tsx | 4 +- apps/web/src/widgets/SearchBox.module.css | 38 +- apps/web/src/widgets/SearchBox.tsx | 20 +- docs/admin-guide.md | 5 + docs/mobile.md | 2 + packages/api-client/src/generated/schema.ts | 42 ++ packages/i18n/locales/en/admin.json | 3 +- packages/i18n/locales/en/auth.json | 14 + packages/i18n/locales/en/common.json | 3 +- packages/i18n/locales/ru/admin.json | 3 +- packages/i18n/locales/ru/auth.json | 14 + packages/i18n/locales/ru/common.json | 3 +- services/api/internal/api/gen/api.gen.go | 590 ++++++++++-------- services/api/internal/auth/auth_test.go | 1 + services/api/internal/auth/middleware.go | 5 +- services/api/internal/library/handler.go | 11 +- services/api/internal/library/service.go | 9 + services/api/internal/library/store.go | 19 +- .../recommender/pipu_rec/analysis/worker.py | 23 + 27 files changed, 757 insertions(+), 277 deletions(-) create mode 100644 apps/web/src/pages/AppDownloadPage.tsx diff --git a/api/openapi.yaml b/api/openapi.yaml index 59e67d1..c40297c 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1006,6 +1006,24 @@ paths: $ref: "#/components/schemas/SearchResults" default: $ref: "#/components/responses/Problem" + /api/v1/admin/library/analysis/retry: + post: + operationId: retryAnalysis + tags: [library] + summary: Вернуть в очередь треки, которые не удалось проанализировать (admin) + responses: + "200": + description: Сколько задач снова в очереди + content: + application/json: + schema: + type: object + required: [requeued] + properties: + requeued: + type: integer + default: + $ref: "#/components/responses/Problem" /api/v1/admin/search/reindex: post: operationId: reindexSearch @@ -1379,6 +1397,7 @@ paths: get: operationId: getAndroidLatest tags: [app] + security: [] # публично: страница /android раздаёт приложение без входа summary: Последняя версия Android-приложения (из релизов Gitea через сервер) description: | Сервер сам опрашивает Gitea своим токеном и кэширует ответ на 10 минут, поэтому приложению @@ -1396,6 +1415,7 @@ paths: get: operationId: downloadAndroidApk tags: [app] + security: [] summary: APK версии (кэшируется на сервере, поддерживает Range для докачки) parameters: - name: version @@ -3081,7 +3101,7 @@ components: description: Рекомендатель недоступен — пачка собрана упрощённо (лайки, любимые исполнители) AnalysisStatus: type: object - required: [queued, running, done, failed, analyzer_ver] + required: [queued, running, done, failed, analyzer_ver, top_error] properties: queued: type: integer @@ -3094,6 +3114,10 @@ components: analyzer_ver: type: string nullable: true + top_error: + type: string + nullable: true + description: Самая частая ошибка среди неудавшихся — подсказка, что чинить WaveMetrics: type: object required: [served, listen_through_rate, early_skip_rate, like_rate, sessions, by_model] diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx index 4dc245c..b255163 100644 --- a/apps/web/src/app/router.tsx +++ b/apps/web/src/app/router.tsx @@ -21,6 +21,7 @@ import { AlbumPage } from '../pages/AlbumPage'; import { ArtistPage } from '../pages/ArtistPage'; import { CollectionPage } from '../pages/CollectionPage'; import { HomePage } from '../pages/HomePage'; +import { AppDownloadPage } from '../pages/AppDownloadPage'; import { LoginPage } from '../pages/LoginPage'; import { SearchPage, type SearchType } from '../pages/SearchPage'; import { SettingsPage } from '../pages/SettingsPage'; @@ -59,6 +60,9 @@ const rootRoute = createRootRouteWithContext()({ errorComponent: RouteError, }); +// Публичная: ссылку на приложение отправляют тем, у кого ещё нет аккаунта на этом устройстве. +const androidAppRoute = createRoute({ getParentRoute: () => rootRoute, path: '/android', component: AppDownloadPage }); + const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: '/login', @@ -233,6 +237,7 @@ const adminSettingsRoute = createRoute({ }); const routeTree = rootRoute.addChildren([ + androidAppRoute, loginRoute, setupAdminRoute, setupPasswordRoute, diff --git a/apps/web/src/pages/AppDownloadPage.tsx b/apps/web/src/pages/AppDownloadPage.tsx new file mode 100644 index 0000000..25aba4d --- /dev/null +++ b/apps/web/src/pages/AppDownloadPage.tsx @@ -0,0 +1,70 @@ +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useSession } from '../app/session'; +import { formatBytes } from '../shared/lib/format'; +import { checkIcon, copyIcon, downloadIcon } from '../shared/ui/icons'; +import { Icon } from '../shared/ui/ui'; +import { AuthLayout } from './AuthLayout'; +import s from './auth.module.css'; + +/** + * Публичная страница Android-приложения (/android): ссылку можно просто отправить человеку. + * APK — последний релиз, который сервер сам берёт из Gitea (docs/mobile.md#самообновление). + */ +export function AppDownloadPage() { + const { t, i18n } = useTranslation(['auth', 'common']); + const session = useSession(); + const [copied, setCopied] = useState(false); + const latest = useQuery({ + queryKey: ['android-latest'], + queryFn: async () => (await session.client.GET('/api/v1/app/android/latest')).data ?? null, + staleTime: 10 * 60_000, + }); + const server = location.origin; + const copy = () => + void navigator.clipboard?.writeText(server).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }); + const r = latest.data; + + return ( + {t('app.to_login')}}> + Android +

{t('app.title')}

+

{t('app.subtitle')}

+ {latest.isPending ? null : r ? ( + <> + + + + {t('app.download')} + + {t('app.version', { version: r.version })} · {formatBytes(r.size, i18n.language)} + + + +
    +
  1. {t('app.step_install')}
  2. +
  3. + {t('app.step_server')} + +
  4. +
  5. {t('app.step_updates')}
  6. +
+
+ {t('app.checksum')} + {r.sha256} +
+ + ) : ( +
{t('app.unavailable')}
+ )} +
+ ); +} diff --git a/apps/web/src/pages/LoginPage.tsx b/apps/web/src/pages/LoginPage.tsx index 2826335..58fa433 100644 --- a/apps/web/src/pages/LoginPage.tsx +++ b/apps/web/src/pages/LoginPage.tsx @@ -37,7 +37,15 @@ export function LoginPage() { }; return ( - + + pipu {info.data.version} · {t('app.link')} + + ) : undefined + } + >

{t('login.title')}

{t('login.subtitle')}

{info.data?.bootstrap_required &&
{t('login.bootstrap_hint')}
} diff --git a/apps/web/src/pages/admin/LibraryPage.tsx b/apps/web/src/pages/admin/LibraryPage.tsx index e6684aa..eae8fb5 100644 --- a/apps/web/src/pages/admin/LibraryPage.tsx +++ b/apps/web/src/pages/admin/LibraryPage.tsx @@ -1,5 +1,5 @@ import { unwrap } from '@pipu/core'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useId } from 'react'; import { useTranslation } from 'react-i18next'; @@ -32,6 +32,11 @@ export function LibraryPage() { const reindex = useMutation({ mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/search/reindex')), }); + const queryClient = useQueryClient(); + const retry = useMutation({ + mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/library/analysis/retry')), + onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['library-status'] }), + }); const d = status.data; const tiles = d ? ([ @@ -77,7 +82,19 @@ export function LibraryPage() { total: d.analysis.done + d.analysis.queued + d.analysis.running + d.analysis.failed, })} /> - {d.analysis.failed > 0 && {t('library.analysis_failed', { count: d.analysis.failed })}} + {d.analysis.failed > 0 && ( +
+ + {t('library.analysis_failed', { count: d.analysis.failed })} + {d.analysis.top_error && {d.analysis.top_error}} + + {admin && ( + + )} +
+ )} {d.analysis.analyzer_ver && {t('library.analyzer', { ver: d.analysis.analyzer_ver })}} )} diff --git a/apps/web/src/pages/admin/library.module.css b/apps/web/src/pages/admin/library.module.css index d38dbfe..8bbbe3c 100644 --- a/apps/web/src/pages/admin/library.module.css +++ b/apps/web/src/pages/admin/library.module.css @@ -118,6 +118,20 @@ color: var(--danger); } +.retryRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.errorDetail { + display: block; + margin-top: 4px; + color: var(--text3); + word-break: break-word; +} + .bar { height: 6px; border-radius: 3px; diff --git a/apps/web/src/pages/auth.module.css b/apps/web/src/pages/auth.module.css index 273dc93..2fafcbc 100644 --- a/apps/web/src/pages/auth.module.css +++ b/apps/web/src/pages/auth.module.css @@ -161,6 +161,83 @@ color: var(--text3); } +.footer a { + color: var(--text2); +} + +/* Страница Android-приложения (/android). */ +.download { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + border-radius: var(--radius-lg); + background: var(--accent-fill); + color: #fff; + font-weight: 700; + text-decoration: none; + box-shadow: var(--glow); + transition: + background 0.2s, + transform 0.15s; +} + +.download:hover { + background: var(--accent-fill2); +} + +.download:active { + transform: scale(0.98); +} + +.downloadMeta { + display: block; + margin-top: 2px; + font-size: 12px; + font-weight: 500; + opacity: 0.85; +} + +.steps { + margin: 0; + padding-left: 20px; + display: flex; + flex-direction: column; + gap: 10px; + color: var(--text2); + font-size: 14px; + line-height: 1.5; +} + +.server { + display: inline-flex; + align-items: center; + gap: 8px; + margin-left: 6px; + padding: 2px 10px; + border-radius: 8px; + border: 1px solid var(--line2); + background: var(--surface2); + color: var(--text); + cursor: pointer; +} + +.details { + font-size: 12px; + color: var(--text3); +} + +.details summary { + cursor: pointer; +} + +.details code { + display: block; + margin-top: 6px; + word-break: break-all; + font-family: var(--font-mono); +} + .link { align-self: center; } diff --git a/apps/web/src/shared/ui/icons.ts b/apps/web/src/shared/ui/icons.ts index c184889..47e2d57 100644 --- a/apps/web/src/shared/ui/icons.ts +++ b/apps/web/src/shared/ui/icons.ts @@ -23,6 +23,8 @@ export { default as albumIcon } from '@material-symbols/svg-400/rounded/album.sv export { default as arrowDownIcon } from '@material-symbols/svg-400/rounded/arrow_downward.svg'; export { default as arrowUpIcon } from '@material-symbols/svg-400/rounded/arrow_upward.svg'; export { default as chevronRightIcon } from '@material-symbols/svg-400/rounded/chevron_right.svg'; +export { default as downloadIcon } from '@material-symbols/svg-400/rounded/download.svg'; +export { default as androidIcon } from '@material-symbols/svg-400/rounded/phone_android.svg'; export { default as deleteIcon } from '@material-symbols/svg-400/rounded/delete.svg'; export { default as editIcon } from '@material-symbols/svg-400/rounded/edit.svg'; export { default as folderIcon } from '@material-symbols/svg-400/rounded/folder.svg'; diff --git a/apps/web/src/widgets/AppShell.tsx b/apps/web/src/widgets/AppShell.tsx index a6d2739..6656aad 100644 --- a/apps/web/src/widgets/AppShell.tsx +++ b/apps/web/src/widgets/AppShell.tsx @@ -22,6 +22,7 @@ import { dislikeFillIcon, dislikeIcon, settingsIcon, + androidIcon, storageFillIcon, storageIcon, inboxFillIcon, inboxIcon, myUploadsFillIcon, myUploadsIcon, uploadFillIcon, uploadIcon, tuneFillIcon, tuneIcon} from '../shared/ui/icons'; import { Avatar, cx, Icon, Logo, Menu } from '../shared/ui/ui'; @@ -102,8 +103,8 @@ function Shell() { -
+ void navigate({ to: '/settings' }) }, + { label: t('profile.android_app'), icon: androidIcon, onSelect: () => void navigate({ to: '/android' }) }, { label: t('profile.logout'), icon: logoutIcon, danger: true, onSelect: () => void session.logout() }, ]} /> diff --git a/apps/web/src/widgets/SearchBox.module.css b/apps/web/src/widgets/SearchBox.module.css index 1bece3a..d296a9a 100644 --- a/apps/web/src/widgets/SearchBox.module.css +++ b/apps/web/src/widgets/SearchBox.module.css @@ -1,12 +1,17 @@ .box { position: relative; - flex: 1; - max-width: 520px; + flex: none; + width: 40px; + transition: width 0.32s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.expanded { + width: min(520px, 46vw); } .icon { position: absolute; - left: 14px; + left: 11px; top: 50%; transform: translateY(-50%); color: var(--text3); @@ -16,13 +21,36 @@ .input { width: 100%; height: 40px; - padding: 0 40px 0 42px; + padding: 0 40px 0 40px; border-radius: var(--radius-pill); border: 1px solid var(--line); background: var(--surface2); color: var(--text); font: inherit; outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s, + background 0.2s; +} + +/* Свёрнутое поле — круглая кнопка: без подсказки и курсора ввода. */ +.box:not(.expanded) .input { + padding: 0; + cursor: pointer; + color: transparent; +} + +.box:not(.expanded) .input::placeholder { + color: transparent; +} + +.box:not(.expanded) .input:hover { + background: var(--surface3); +} + +.box:not(.expanded) .kbd { + display: none; } .input:focus { @@ -56,8 +84,8 @@ .popover { position: absolute; - left: 0; right: 0; + width: min(520px, calc(100vw - 32px)); top: calc(100% + 8px); z-index: 45; max-height: min(70vh, 560px); diff --git a/apps/web/src/widgets/SearchBox.tsx b/apps/web/src/widgets/SearchBox.tsx index a7bfde4..e1ce826 100644 --- a/apps/web/src/widgets/SearchBox.tsx +++ b/apps/web/src/widgets/SearchBox.tsx @@ -23,7 +23,10 @@ type Item = { go: () => void; }; -/** Поиск в топбаре: подсказки по мере ввода, Enter — страница поиска или выбранный элемент. */ +/** + * Поиск в топбаре: подсказки по мере ввода, Enter — страница поиска или выбранный элемент. + * Без фокуса и текста свёрнут в кнопку с лупой; поле раскрывается влево с анимацией. + */ export function SearchBox() { const { t } = useTranslation(); const session = useSession(); @@ -34,6 +37,9 @@ export function SearchBox() { const [debounced, setDebounced] = useState(''); const [open, setOpen] = useState(false); const [active, setActive] = useState(-1); + const [focused, setFocused] = useState(false); + // Свёрнут в круглую кнопку, пока не нужен: раскрывается по клику, «/» или Ctrl+K. + const expanded = focused || q !== ''; useEffect(() => { const id = setTimeout(() => setDebounced(q.trim()), DEBOUNCE_MS); @@ -141,7 +147,7 @@ export function SearchBox() { const optionId = (i: number) => `${listId}-${i}`; return ( -
+
setOpen(true)} - onBlur={() => setTimeout(close, 120)} + onFocus={() => { + setFocused(true); + setOpen(true); + }} + onBlur={() => { + setFocused(false); + setTimeout(close, 120); + }} onKeyDown={onKeyDown} /> diff --git a/docs/admin-guide.md b/docs/admin-guide.md index 7b3428f..1fca3c5 100644 --- a/docs/admin-guide.md +++ b/docs/admin-guide.md @@ -49,6 +49,9 @@ индекс обновляется сам и пересобирается при старте api). - **Аудиоанализ** (темп, энергия, настроение — для «Моей волны») идёт в фоне: на 10 тыс. треков — несколько часов при 4 ядрах (`analyzer` в `compose.yaml`, `--workers`). Волна работает и до его окончания, но точнее — после. + Не удалось проанализировать часть треков (например, пока диск не был смонтирован) — «Повторить» под счётчиком + ошибок возвращает их в очередь; рядом показана самая частая ошибка. Пропавшие файлы анализатор не считает + попыткой и ждёт, пока диск вернётся. ## 4. Очередь на добавление @@ -91,6 +94,8 @@ прочитайте заметки к релизу на странице релизов Gitea. Android-приложение обновляется само: при запуске оно сверяется с сервером и предлагает новый APK. +Раздать приложение — ссылка `https://<ваш-домен>/android` (без входа): кнопка «Скачать APK» последнего релиза, +адрес сервера для копирования и короткая инструкция; та же страница — внизу экрана входа и в меню профиля. ## 7. Бэкапы и восстановление diff --git a/docs/mobile.md b/docs/mobile.md index 58ca59a..4a4f7f2 100644 --- a/docs/mobile.md +++ b/docs/mobile.md @@ -82,6 +82,8 @@ ## Самообновление +Первая установка — публичная страница `/android` веб-клиента: последний APK, адрес сервера и инструкция. + Приложение распространяется только через Gitea Releases (не Google Play). **Поток:** diff --git a/packages/api-client/src/generated/schema.ts b/packages/api-client/src/generated/schema.ts index acc9b19..f6faede 100644 --- a/packages/api-client/src/generated/schema.ts +++ b/packages/api-client/src/generated/schema.ts @@ -880,6 +880,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/library/analysis/retry": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Вернуть в очередь треки, которые не удалось проанализировать (admin) */ + post: operations["retryAnalysis"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/search/reindex": { parameters: { query?: never; @@ -2012,6 +2029,8 @@ export interface components { done: number; failed: number; analyzer_ver: string | null; + /** @description Самая частая ошибка среди неудавшихся — подсказка, что чинить */ + top_error: string | null; }; /** @description Метрики волны за 7 дней */ WaveMetrics: { @@ -3355,6 +3374,29 @@ export interface operations { default: components["responses"]["Problem"]; }; }; + retryAnalysis: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Сколько задач снова в очереди */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + requeued: number; + }; + }; + }; + default: components["responses"]["Problem"]; + }; + }; reindexSearch: { parameters: { query?: never; diff --git a/packages/i18n/locales/en/admin.json b/packages/i18n/locales/en/admin.json index fe9b115..53008e0 100644 --- a/packages/i18n/locales/en/admin.json +++ b/packages/i18n/locales/en/admin.json @@ -160,7 +160,8 @@ "wave_early_skip": "Early skips", "wave_likes": "Likes per 100", "wave_model": "Model version", - "wave_by_model": "Wave model versions compared" + "wave_by_model": "Wave model versions compared", + "analysis_retry": "Retry" }, "write_template": { "title": "Uploads and path template", diff --git a/packages/i18n/locales/en/auth.json b/packages/i18n/locales/en/auth.json index aaed601..04ce974 100644 --- a/packages/i18n/locales/en/auth.json +++ b/packages/i18n/locales/en/auth.json @@ -20,5 +20,19 @@ "min_length": "At least {{min}} characters", "submit": "Save and continue", "logout": "Sign out" + }, + "app": { + "link": "Android app", + "title": "pipu for Android", + "subtitle": "Your music from this server on your phone: My Wave, your collection and offline listening.", + "download": "Download APK", + "version": "version {{version}}", + "step_install": "Open the downloaded file. If Android asks, allow installs from this source.", + "step_server": "Launch pipu and enter the server address:", + "copy_server": "Copy the server address", + "step_updates": "Sign in with your account. The app offers new versions by itself.", + "checksum": "SHA-256 checksum", + "unavailable": "The app is not available yet: release downloads are not configured on this server (docs/deployment.md).", + "to_login": "Sign in to the web app" } } diff --git a/packages/i18n/locales/en/common.json b/packages/i18n/locales/en/common.json index 2b14df4..f8bfc55 100644 --- a/packages/i18n/locales/en/common.json +++ b/packages/i18n/locales/en/common.json @@ -39,7 +39,8 @@ "profile": { "menu": "Profile menu", "settings": "Settings", - "logout": "Sign out" + "logout": "Sign out", + "android_app": "Android app" }, "roles": { "admin": "Administrator", diff --git a/packages/i18n/locales/ru/admin.json b/packages/i18n/locales/ru/admin.json index 9f6789b..66dd917 100644 --- a/packages/i18n/locales/ru/admin.json +++ b/packages/i18n/locales/ru/admin.json @@ -160,7 +160,8 @@ "wave_early_skip": "Ранние скипы", "wave_likes": "Лайков на 100", "wave_model": "Версия модели", - "wave_by_model": "Сравнение версий модели волны" + "wave_by_model": "Сравнение версий модели волны", + "analysis_retry": "Повторить" }, "write_template": { "title": "Приём загрузок и шаблон пути", diff --git a/packages/i18n/locales/ru/auth.json b/packages/i18n/locales/ru/auth.json index 55d6b50..32fac38 100644 --- a/packages/i18n/locales/ru/auth.json +++ b/packages/i18n/locales/ru/auth.json @@ -20,5 +20,19 @@ "min_length": "Не короче {{min}} символов", "submit": "Сохранить и продолжить", "logout": "Выйти" + }, + "app": { + "link": "Приложение для Android", + "title": "pipu для Android", + "subtitle": "Ваша музыка с этого сервера на телефоне: «Моя волна», коллекция и офлайн-прослушивание.", + "download": "Скачать APK", + "version": "версия {{version}}", + "step_install": "Откройте скачанный файл. Если Android спросит — разрешите установку из этого источника.", + "step_server": "Запустите pipu и введите адрес сервера:", + "copy_server": "Скопировать адрес сервера", + "step_updates": "Войдите своим логином. Новые версии приложение предложит установить само.", + "checksum": "Контрольная сумма SHA-256", + "unavailable": "Приложение пока недоступно: на сервере не настроена загрузка релизов (docs/deployment.md).", + "to_login": "Войти в веб-версию" } } diff --git a/packages/i18n/locales/ru/common.json b/packages/i18n/locales/ru/common.json index a329199..66a6f8e 100644 --- a/packages/i18n/locales/ru/common.json +++ b/packages/i18n/locales/ru/common.json @@ -39,7 +39,8 @@ "profile": { "menu": "Меню профиля", "settings": "Настройки", - "logout": "Выйти" + "logout": "Выйти", + "android_app": "Приложение для Android" }, "roles": { "admin": "Администратор", diff --git a/services/api/internal/api/gen/api.gen.go b/services/api/internal/api/gen/api.gen.go index 6b26677..5755964 100644 --- a/services/api/internal/api/gen/api.gen.go +++ b/services/api/internal/api/gen/api.gen.go @@ -288,6 +288,9 @@ type AnalysisStatus struct { Failed int `json:"failed"` Queued int `json:"queued"` Running int `json:"running"` + + // TopError Самая частая ошибка среди неудавшихся — подсказка, что чинить + TopError *string `json:"top_error"` } // AndroidRelease defines model for AndroidRelease. @@ -1464,6 +1467,9 @@ type ServerInterface interface { // Журнал действий (admin), новые сверху // (GET /api/v1/admin/audit) ListAudit(w http.ResponseWriter, r *http.Request, params ListAuditParams) + // Вернуть в очередь треки, которые не удалось проанализировать (admin) + // (POST /api/v1/admin/library/analysis/retry) + RetryAnalysis(w http.ResponseWriter, r *http.Request) // Файлы, которые не удалось прочитать (admin) // (GET /api/v1/admin/library/errors) ListLibraryErrors(w http.ResponseWriter, r *http.Request, params ListLibraryErrorsParams) @@ -1695,6 +1701,12 @@ func (_ Unimplemented) ListAudit(w http.ResponseWriter, r *http.Request, params w.WriteHeader(http.StatusNotImplemented) } +// Вернуть в очередь треки, которые не удалось проанализировать (admin) +// (POST /api/v1/admin/library/analysis/retry) +func (_ Unimplemented) RetryAnalysis(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Файлы, которые не удалось прочитать (admin) // (GET /api/v1/admin/library/errors) func (_ Unimplemented) ListLibraryErrors(w http.ResponseWriter, r *http.Request, params ListLibraryErrorsParams) { @@ -2183,6 +2195,26 @@ func (siw *ServerInterfaceWrapper) ListAudit(w http.ResponseWriter, r *http.Requ handler.ServeHTTP(w, r) } +// RetryAnalysis operation middleware +func (siw *ServerInterfaceWrapper) RetryAnalysis(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RetryAnalysis(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListLibraryErrors operation middleware func (siw *ServerInterfaceWrapper) ListLibraryErrors(w http.ResponseWriter, r *http.Request) { @@ -3057,12 +3089,6 @@ func (siw *ServerInterfaceWrapper) DownloadAndroidApk(w http.ResponseWriter, r * return } - ctx := r.Context() - - ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) - - r = r.WithContext(ctx) - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.DownloadAndroidApk(w, r, version) })) @@ -3077,12 +3103,6 @@ func (siw *ServerInterfaceWrapper) DownloadAndroidApk(w http.ResponseWriter, r * // GetAndroidLatest operation middleware func (siw *ServerInterfaceWrapper) GetAndroidLatest(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) - - r = r.WithContext(ctx) - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetAndroidLatest(w, r) })) @@ -4455,6 +4475,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/admin/audit", wrapper.ListAudit) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/api/v1/admin/library/analysis/retry", wrapper.RetryAnalysis) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/admin/library/errors", wrapper.ListLibraryErrors) }) @@ -4709,6 +4732,36 @@ func (response ListAuditdefaultApplicationProblemPlusJSONResponse) VisitListAudi return json.NewEncoder(w).Encode(response.Body) } +type RetryAnalysisRequestObject struct { +} + +type RetryAnalysisResponseObject interface { + VisitRetryAnalysisResponse(w http.ResponseWriter) error +} + +type RetryAnalysis200JSONResponse struct { + Requeued int `json:"requeued"` +} + +func (response RetryAnalysis200JSONResponse) VisitRetryAnalysisResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RetryAnalysisdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response RetryAnalysisdefaultApplicationProblemPlusJSONResponse) VisitRetryAnalysisResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type ListLibraryErrorsRequestObject struct { Params ListLibraryErrorsParams } @@ -6832,6 +6885,9 @@ type StrictServerInterface interface { // Журнал действий (admin), новые сверху // (GET /api/v1/admin/audit) ListAudit(ctx context.Context, request ListAuditRequestObject) (ListAuditResponseObject, error) + // Вернуть в очередь треки, которые не удалось проанализировать (admin) + // (POST /api/v1/admin/library/analysis/retry) + RetryAnalysis(ctx context.Context, request RetryAnalysisRequestObject) (RetryAnalysisResponseObject, error) // Файлы, которые не удалось прочитать (admin) // (GET /api/v1/admin/library/errors) ListLibraryErrors(ctx context.Context, request ListLibraryErrorsRequestObject) (ListLibraryErrorsResponseObject, error) @@ -7108,6 +7164,30 @@ func (sh *strictHandler) ListAudit(w http.ResponseWriter, r *http.Request, param } } +// RetryAnalysis operation middleware +func (sh *strictHandler) RetryAnalysis(w http.ResponseWriter, r *http.Request) { + var request RetryAnalysisRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RetryAnalysis(ctx, request.(RetryAnalysisRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RetryAnalysis") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RetryAnalysisResponseObject); ok { + if err := validResponse.VisitRetryAnalysisResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ListLibraryErrors operation middleware func (sh *strictHandler) ListLibraryErrors(w http.ResponseWriter, r *http.Request, params ListLibraryErrorsParams) { var request ListLibraryErrorsRequestObject @@ -9130,248 +9210,250 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/services/api/internal/auth/auth_test.go b/services/api/internal/auth/auth_test.go index c4b4d5e..006353d 100644 --- a/services/api/internal/auth/auth_test.go +++ b/services/api/internal/auth/auth_test.go @@ -565,5 +565,6 @@ func TestPublicRoutes(t *testing.T) { e := newEnv(t) assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/server-info", nil).Code) assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/healthz", nil).Code) + assert.NotEqual(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/app/android/latest", nil).Code, "APK page is public") assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/unknown", nil).Code) } diff --git a/services/api/internal/auth/middleware.go b/services/api/internal/auth/middleware.go index 0241010..4a9aefa 100644 --- a/services/api/internal/auth/middleware.go +++ b/services/api/internal/auth/middleware.go @@ -55,6 +55,8 @@ var publicRoutes = map[string]bool{ "GET /api/v1/server-info": true, "POST /api/v1/auth/login": true, "POST /api/v1/auth/refresh": true, + // Android-приложение раздаётся по ссылке /android без входа: в APK нет ничего, кроме клиента. + "GET /api/v1/app/android/latest": true, } // Что разрешено, пока пользователь обязан сменить учётные данные / пароль. @@ -90,7 +92,8 @@ func Middleware(svc *Service, log *slog.Logger) func(http.Handler) http.Handler // Обложки открывает без заголовка Authorization — ссылку защищает подпись. // Аудио открывает