From 39e1109c75002a5a74d525c3a009a6153f60575a Mon Sep 17 00:00:00 2001
From: Moriel
Date: Wed, 17 Jun 2026 16:54:08 +0300
Subject: [PATCH] Simplify upload flow to the real upscaler
The frontend advertised five "products" (Denoise/Deblur/Artifacts/Pro)
that the backend never read and that don't exist in the AI pipeline.
Removes the fake catalog/nav dropdown/Pro tier in favor of a single
upload flow for the one real model, and rewrites the Technology page
to accurately describe BasicVSR + SPyNet (explicit flow-aligned
recurrent propagation) instead of the made-up channel-concat CNN.
Co-Authored-By: Claude Sonnet 4.6
---
apps/backend/AGENTS.md | 1 -
apps/backend/src/upload/upload.controller.ts | 3 +-
apps/frontend/AGENTS.md | 10 +-
apps/frontend/README.md | 13 +-
apps/frontend/src/consts/products.ts | 63 ----
apps/frontend/src/router/index.ts | 2 -
apps/frontend/src/ui/components/Footer.tsx | 2 +-
apps/frontend/src/ui/components/Navbar.tsx | 146 ++------
.../src/ui/components/home/HeroSection.tsx | 2 +-
.../technology/ArchitectureSection.tsx | 63 ++--
.../components/technology/PipelineSection.tsx | 76 ++--
.../src/ui/components/technology/TechHero.tsx | 17 +-
apps/frontend/src/ui/pages/Product.tsx | 288 ---------------
apps/frontend/src/ui/pages/Products.tsx | 348 +++++++++++-------
14 files changed, 358 insertions(+), 676 deletions(-)
delete mode 100644 apps/frontend/src/consts/products.ts
delete mode 100644 apps/frontend/src/ui/pages/Product.tsx
diff --git a/apps/backend/AGENTS.md b/apps/backend/AGENTS.md
index 8154f02..1dc3718 100644
--- a/apps/backend/AGENTS.md
+++ b/apps/backend/AGENTS.md
@@ -65,7 +65,6 @@ States: `queued → processing → completed | failed | cancelled`. Terminal sta
- Jobs live only in memory: lost on restart, never cleaned up (disk files included). No queue — concurrent uploads hit the AI service concurrently. No auth or rate limiting.
- `result.downloadUrl` is a relative path (`/api/upload/stream/{jobId}`); `outputFilename` is a display name (`{base}_enhanced_by_upscale{ext}`) — the file on disk is `{jobId}_enhanced{ext}`.
-- The Swagger-documented `product` form field on upload is accepted but never read.
- Multer's `fileFilter` rejects via plain `Error`, so a rejected extension may not produce a clean ProblemDetails.
- Range parsing does not bounds-check `start`/`end` against the file size.
diff --git a/apps/backend/src/upload/upload.controller.ts b/apps/backend/src/upload/upload.controller.ts
index 78efa54..83d2c12 100644
--- a/apps/backend/src/upload/upload.controller.ts
+++ b/apps/backend/src/upload/upload.controller.ts
@@ -61,8 +61,7 @@ export class UploadController {
schema: {
type: 'object',
properties: {
- video: { type: 'string', format: 'binary' },
- product: { type: 'string' }
+ video: { type: 'string', format: 'binary' }
},
required: ['video']
}
diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md
index ccc35f8..e883358 100644
--- a/apps/frontend/AGENTS.md
+++ b/apps/frontend/AGENTS.md
@@ -7,20 +7,20 @@ Vite 8 + React 19 SPA. Port 5173 (`VITE_PORT`). Tailwind v4 (CSS-config in `src/
## Structure
- `src/main.tsx` / `src/App.tsx` — entry (StrictMode, Redux `Provider`) and `RouterProvider`.
-- `src/router/index.ts` — routes: Home, Products, Product (`:slug`), Technology, About, all nested under `RootLayout` (Navbar + Footer).
+- `src/router/index.ts` — routes: Home, Products (the single upload tool, no slug), Technology, About, all nested under `RootLayout` (Navbar + Footer).
- `src/store/` — Redux store; `store/api/upscale.api.ts` is the RTK Query API (upload with XHR progress, status, result, cancel); `store/slices/job.slice.ts` holds `activeJobs` (currently write-only scaffolding — nothing reads it).
- `src/config/api.ts` — `API_ORIGIN` from `VITE_API_BASE_URL` (origin only, no `/api` — it strips a legacy `/api` suffix), plus `buildApiUrl`/`interpolatePath` helpers for contract paths.
- `src/ui/pages|components|layouts` — feature components; `src/ui/shadcn/` is vendored shadcn (lint-relaxed, avoid editing). `cn()` lives at `@/ui/shadcn/lib/utils` (the `components.json` `utils` alias is stale — use the lib path).
-- `src/consts/` — frontend-only UI data (`products.ts` catalog with `isPro`/`isWip` flags, navigation, features).
+- `src/consts/` — frontend-only UI data (navigation, features).
- `src/utils/format.ts` — `formatFileSize` (used), `formatDuration` (currently unused).
## The upscale flow
-Everything lives under `/products/:slug`. Only `upscaler` and `pro` accept uploads; WIP slugs show a "Coming Soon" card; unknown slugs redirect to `/products/upscaler`.
+Everything lives under `/products` — a single page for the one real model the AI service runs (BasicVSR + SPyNet, fixed 4x super-resolution). There used to be a multi-"product" catalog (Denoise/Deblur/Artifacts/Pro) and a `/products/:slug` route, but the backend never read the slug and those tools don't exist in the inference pipeline — they were removed rather than left as misleading marketing.
-`src/ui/pages/Product.tsx` orchestrates with a **local** `PageState` machine (`idle | uploading | processing | completed | failed | cancelled`) in `useState` — not Redux. A page refresh loses in-flight job UI state.
+`src/ui/pages/Products.tsx` orchestrates with a **local** `PageState` machine (`idle | uploading | processing | completed | failed | cancelled`) in `useState` — not Redux. A page refresh loses in-flight job UI state.
-1. **Upload** — `VideoUploadForm.tsx` (drag-and-drop, MIME allowlist, 500 MB client cap). `uploadVideo` in `upscale.api.ts` uses a **custom XHR in `queryFn`** because fetch has no upload progress events — do not refactor it to a plain `builder.mutation`/fetch. FormData fields: `video` (file) and `product` (slug; backend currently ignores it). Errors are parsed as RFC 7807 (`detail`/`title`).
+1. **Upload** — `VideoUploadForm.tsx` (drag-and-drop, MIME allowlist, 500 MB client cap). `uploadVideo` in `upscale.api.ts` uses a **custom XHR in `queryFn`** because fetch has no upload progress events — do not refactor it to a plain `builder.mutation`/fetch. FormData fields: `video` (file) only. Errors are parsed as RFC 7807 (`detail`/`title`).
2. **Live updates** — `JobStatusPanel.tsx` opens a native `EventSource` to `UPLOAD_EVENTS_ENDPOINT` (SSE is **outside** RTK Query). Each message is validated with `jobUpdateSchema`. On SSE failure it falls back to polling `getJobStatusContract.path` every 1s with raw `fetch`, giving up after 30 attempts. Terminal states close the stream and notify the parent. (`useGetJobStatusQuery` exists in the RTK API but is unused by the UI.)
3. **Cancel** — `useCancelJobMutation`; UI sets `isStopping` and waits for SSE/polling to report `cancelled` (the terminal transition is asynchronous).
4. **Result** — `JobResultPanel.tsx` fetches metadata via `useGetJobResultQuery`, plays the video from `buildApiUrl(UPLOAD_STREAM_ENDPOINT, { jobId })` (HTTP Range), and downloads by fetching the stream as a blob named `result.outputFilename`. **The `downloadUrl` field from the API is ignored** — always build the stream URL from `@repo/consts/upload`.
diff --git a/apps/frontend/README.md b/apps/frontend/README.md
index bea2c6f..3a6c485 100644
--- a/apps/frontend/README.md
+++ b/apps/frontend/README.md
@@ -15,13 +15,12 @@ The backend (`apps/backend`, port 3000) must be running for the upscale flow; fo
## Pages
-| Route | Page |
-| ----------------- | ---------------------------------------------------------------- |
-| `/` | Home (hero, features, how-it-works) |
-| `/products` | Product catalog |
-| `/products/:slug` | Product page — `upscaler` and `pro` host the working upload flow |
-| `/technology` | Tech overview (pipeline, architecture, stack) |
-| `/about` | Project, team, academic context |
+| Route | Page |
+| -------------- | ------------------------------------------------ |
+| `/` | Home (hero, features, how-it-works) |
+| `/products` | Video Upscaler — the one working upload flow |
+| `/technology` | Tech overview (pipeline, architecture, stack) |
+| `/about` | Project, team, academic context |
## Configuration
diff --git a/apps/frontend/src/consts/products.ts b/apps/frontend/src/consts/products.ts
deleted file mode 100644
index 9fd0808..0000000
--- a/apps/frontend/src/consts/products.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { ZoomIn, Eraser, Focus, Wand2, Crown } from 'lucide-react';
-import type { LucideIcon } from 'lucide-react';
-
-export interface ProductDefinition {
- slug: string;
- name: string;
- shortName: string;
- description: string;
- icon: LucideIcon;
- isPro?: boolean;
- isWip?: boolean;
-}
-
-export const PRODUCTS: ProductDefinition[] = [
- {
- slug: 'upscaler',
- name: 'Video Upscaler',
- shortName: 'Upscaler',
- description:
- 'Increase video resolution up to 4x. Recover fine spatial details lost in low-resolution recordings using deep learning super-resolution.',
- icon: ZoomIn
- },
- {
- slug: 'denoise',
- name: 'Noise Reducer',
- shortName: 'Denoise',
- description:
- 'Remove grain, sensor noise, and analog artifacts from legacy video content while preserving texture and detail.',
- icon: Eraser,
- isWip: true
- },
- {
- slug: 'deblur',
- name: 'Blur Fix',
- shortName: 'Deblur',
- description:
- 'Correct focus issues and motion blur. Sharpen soft footage to reveal details hidden by optical and motion degradation.',
- icon: Focus,
- isWip: true
- },
- {
- slug: 'artifacts',
- name: 'Artifact Cleaner',
- shortName: 'Artifacts',
- description:
- 'Eliminate compression blocks, ringing effects, and encoding artifacts introduced by aggressive video compression.',
- icon: Wand2,
- isWip: true
- },
- {
- slug: 'pro',
- name: 'Upscale Pro',
- shortName: 'Pro',
- description:
- 'The complete restoration pipeline. Combines super-resolution, denoising, deblurring, and artifact removal in a single pass for maximum quality.',
- icon: Crown,
- isPro: true
- }
-];
-
-export function getProductBySlug(slug: string): ProductDefinition | undefined {
- return PRODUCTS.find((p) => p.slug === slug);
-}
diff --git a/apps/frontend/src/router/index.ts b/apps/frontend/src/router/index.ts
index ace6009..2d43c45 100644
--- a/apps/frontend/src/router/index.ts
+++ b/apps/frontend/src/router/index.ts
@@ -3,7 +3,6 @@ import { Root } from '@/ui/Root';
import { RootLayout } from '@/ui/layouts/RootLayout';
import { Home } from '@/ui/pages/Home';
import { Products } from '@/ui/pages/Products';
-import { Product } from '@/ui/pages/Product';
import { Technology } from '@/ui/pages/Technology';
import { About } from '@/ui/pages/About';
@@ -17,7 +16,6 @@ export const router = createBrowserRouter([
children: [
{ index: true, Component: Home },
{ path: 'products', Component: Products },
- { path: 'products/:slug', Component: Product },
{ path: 'technology', Component: Technology },
{ path: 'about', Component: About }
]
diff --git a/apps/frontend/src/ui/components/Footer.tsx b/apps/frontend/src/ui/components/Footer.tsx
index 9100ac2..e8a7c9c 100644
--- a/apps/frontend/src/ui/components/Footer.tsx
+++ b/apps/frontend/src/ui/components/Footer.tsx
@@ -43,7 +43,7 @@ export function Footer() {
to="/products"
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
- Products
+ Upscale Video
diff --git a/apps/frontend/src/ui/components/Navbar.tsx b/apps/frontend/src/ui/components/Navbar.tsx
index b553244..fbfe882 100644
--- a/apps/frontend/src/ui/components/Navbar.tsx
+++ b/apps/frontend/src/ui/components/Navbar.tsx
@@ -1,18 +1,12 @@
import { useState } from 'react';
-import { NavLink, Link, useLocation } from 'react-router';
-import { Menu, X, ChevronDown, Crown } from 'lucide-react';
+import { NavLink, Link } from 'react-router';
+import { Menu, X, Upload } from 'lucide-react';
import { cn } from '@/ui/shadcn/lib/utils';
import { Button } from '@/ui/shadcn/ui/button';
import { NAV_LINKS_BEFORE, NAV_LINKS_AFTER } from '@/consts/navigation';
-import { PRODUCTS } from '@/consts/products';
export function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
- const location = useLocation();
- const isProductsActive = location.pathname.startsWith('/products');
-
- const freeProducts = PRODUCTS.filter((p) => !p.isPro);
- const proProduct = PRODUCTS.find((p) => p.isPro);
return (
@@ -43,61 +37,19 @@ export function Navbar() {
))}
-