mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 18:06:45 +08:00
* feat(web): make the mobile web app installable (MUL-6237) Adds the first tier of PWA support: the app can be added to a phone's home screen and runs in its own window instead of a browser tab. - app/manifest.ts, served at /manifest.webmanifest. start_url is /inbox rather than /, because the official marketing hosts keep the root path on the public site even for a signed-in session, so an installed app pointed at / would open the landing page. /inbox is a legacy segment that proxy.ts already resolves per session: the last workspace when signed in, /login when not. manifest.test.ts runs start_url and both launcher shortcuts through the real proxy so that coupling cannot rot. - Home-screen icons generated from the existing brand marks, so an installed web app matches the desktop app. The maskable one is full-bleed with the mark at 52% to stay inside the safe zone. - public/sw.js, which caches nothing. Chrome only offers to install a site whose worker has a fetch handler, and landing skipWaiting + clients.claim + a cache sweep now means a later caching change adds a strategy instead of retrofitting a safe upgrade path. Registration is skipped in development, where a worker only ever serves stale bundles. Offline support and web push are deliberately out of scope. Co-authored-by: multica-agent <[email protected]> * fix(web): resolve the PWA launch path for a session with no workspace (MUL-6237) Review found the installed app could open the marketing site. `start_url` is /inbox, a legacy segment, and proxy.ts bounced a request carrying `multica_logged_in` but no `last_workspace_slug` to "/" — which the official marketing hosts deliberately keep on the public site even with a session. The bounce therefore dead-ended on the landing page, and for a launcher icon there is no URL bar to recover with. Reachable whenever the session cookie outlives the slug cookie: a member who signed up but has not opened a workspace yet, or cleared cookies. Send that state to /login instead, which already resolves an authenticated visitor against their workspace list, including pending invitations and the no-workspace-yet case. The deep-link path is dropped rather than passed as `next`, because a legacy segment fed back through `next` would return to this branch and loop. This also fixes the same dead end for plain legacy bookmarks on the marketing hosts. Also drops the service worker. It only ever existed to satisfy Chrome's installability check, and that check no longer needs one: menu install dropped the fetch-handler requirement in Chrome 108 (Android) / 112 (desktop). Only the automatic install prompt still requires a handler, which this app has no custom UI for anyway — so the worker bought a prompt heuristic in exchange for an empty fetch handler Chrome's own guidance warns against, plus a sticky registration needing an unregister path. Android installs from the menu, iOS from the share sheet. The worker comes back with tier 2, when there is something real to cache. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]>
128 lines
4.8 KiB
TypeScript
128 lines
4.8 KiB
TypeScript
import { NextResponse, type NextRequest } from "next/server";
|
|
import { LOCALE_COOKIE } from "@multica/core/i18n";
|
|
import {
|
|
MULTICA_LOCALE_HEADER,
|
|
resolveLocaleFromSignals,
|
|
} from "./lib/locale-routing";
|
|
import { runtimeRewriteDestination } from "./config/runtime-urls";
|
|
import { isOfficialMarketingHost } from "./lib/public-host";
|
|
|
|
// Old workspace-scoped route segments that existed before the URL refactor
|
|
// (pre-#1131). Any URL with these as the FIRST segment is a legacy URL that
|
|
// needs to be rewritten to /{slug}/{route}/... so old bookmarks, deep links,
|
|
// and post-revert-and-reapply users don't hit 404.
|
|
const LEGACY_ROUTE_SEGMENTS = new Set([
|
|
"issues",
|
|
"projects",
|
|
"agents",
|
|
"squads",
|
|
"inbox",
|
|
"my-issues",
|
|
"autopilots",
|
|
"runtimes",
|
|
"skills",
|
|
"settings",
|
|
"usage",
|
|
]);
|
|
|
|
function resolveLocale(req: NextRequest): string {
|
|
return resolveLocaleFromSignals({
|
|
cookieLocale: req.cookies.get(LOCALE_COOKIE)?.value,
|
|
acceptLanguage: req.headers.get("accept-language"),
|
|
});
|
|
}
|
|
|
|
// Forward the resolved locale to RSC layouts via the `x-multica-locale`
|
|
// request header. layout.tsx reads it through `await headers()`. The
|
|
// `request: { headers }` form is what makes the header land on the upstream
|
|
// request — without it the value would only sit on the response.
|
|
function nextWithLocale(req: NextRequest): NextResponse {
|
|
const headers = new Headers(req.headers);
|
|
headers.set(MULTICA_LOCALE_HEADER, resolveLocale(req));
|
|
return NextResponse.next({ request: { headers } });
|
|
}
|
|
|
|
// Next.js 16 renamed `middleware` → `proxy`. API surface (NextRequest /
|
|
// NextResponse / cookies / matcher) is identical; the only behavioral
|
|
// change is the runtime — proxy is forced to nodejs and cannot opt into
|
|
// edge.
|
|
export function proxy(req: NextRequest) {
|
|
const { pathname } = req.nextUrl;
|
|
const runtimeDestination = runtimeRewriteDestination(pathname, process.env);
|
|
if (runtimeDestination) {
|
|
const url = new URL(runtimeDestination);
|
|
url.search = req.nextUrl.search;
|
|
return NextResponse.rewrite(url);
|
|
}
|
|
|
|
const hasSession = req.cookies.has("multica_logged_in");
|
|
const lastSlug = req.cookies.get("last_workspace_slug")?.value;
|
|
|
|
// --- Legacy URL redirect: /issues/... → /{slug}/issues/... ---
|
|
// Old bookmarks and clients that hit us before the slug migration would
|
|
// otherwise 404 since the route moved under [workspaceSlug].
|
|
const firstSegment = pathname.split("/")[1] ?? "";
|
|
if (LEGACY_ROUTE_SEGMENTS.has(firstSegment)) {
|
|
const url = req.nextUrl.clone();
|
|
|
|
if (!hasSession) {
|
|
url.pathname = "/login";
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
if (lastSlug) {
|
|
// Preserve deep-link path + query: /issues/abc → /{lastSlug}/issues/abc
|
|
url.pathname = `/${lastSlug}${pathname}`;
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
// Logged-in but no cookie yet (never opened a workspace, or the cookie was
|
|
// cleared). Root is the wrong destination: the root-path rule below leaves
|
|
// `/` on the public site for the official marketing hosts even with a
|
|
// session, so bouncing there dead-ends on the landing page instead of
|
|
// reaching the app. /login already resolves an authenticated visitor
|
|
// against their workspace list — including pending invitations and the
|
|
// no-workspace-yet case — and replaces to the right destination. Deep-link
|
|
// path and query are dropped rather than passed as `next`: they are legacy
|
|
// segments themselves, so feeding one back would land here again.
|
|
url.pathname = "/login";
|
|
url.search = "";
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
// --- Root path: redirect logged-in users to their last workspace ---
|
|
// The official cloud host also serves the public marketing site. Visiting
|
|
// https://multica.ai/ must remain a public-site navigation even when a local
|
|
// desktop/runtime session has fresh auth cookies; explicit app routes such
|
|
// as /acme/issues and legacy /issues still route to the workspace app.
|
|
if (
|
|
pathname === "/" &&
|
|
hasSession &&
|
|
lastSlug &&
|
|
!isOfficialMarketingHost(req.nextUrl.hostname)
|
|
) {
|
|
const url = req.nextUrl.clone();
|
|
url.pathname = `/${lastSlug}/issues`;
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
// --- Default: forward locale header to RSC, no redirect/rewrite ---
|
|
// Covers logged-out root path, /login, /:slug/*, and everything else.
|
|
return nextWithLocale(req);
|
|
}
|
|
|
|
export const config = {
|
|
// i18n header must land on every page request, so we use the standard
|
|
// negative-lookahead pattern from Next's i18n guide, plus explicit runtime
|
|
// proxy routes whose upstream origins are resolved from process.env at
|
|
// request time instead of being baked into next.config.js at build time.
|
|
matcher: [
|
|
"/api/:path*",
|
|
"/auth/:path*",
|
|
"/uploads/:path*",
|
|
"/docs/:path*",
|
|
"/ws",
|
|
"/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)",
|
|
],
|
|
};
|