chore: sync the frontend template from a0fbafb to 5434760
Carries eight template commits: the provider sandbox actually running, release
admission to a named environment, the product feature manifest with its runtime
kill switch, architecture and documentation rules that match what is enforced,
the removability fixtures, and the browser, visual and performance evidence.
Product identity is unchanged. `package.json` keeps `tech-log-frontend` and the
catalog keeps the Tech Log naming; the home page was not in the delta. The
visual baselines are this product's own — the template's were excluded from the
transplant and these were regenerated here, where the only difference is the
platform overview's new product-feature section.
What this repository gains operationally: `config/runtime/{local,development,
staging,production}.json` with `FE-GATE-027` refusing an artifact whose runtime
document does not match the environment it is being admitted to, and
`FEATURE_OVERRIDES` for taking an installed feature out of service without a
rebuild.
Verified here: eight gates green, build green, visual 5/5, and 1,858 of 1,859
tests in the suites that do not need a sandbox — the one failure passes in
isolation and is a jsdom lazy-chunk timeout under parallel load. The provider
suites cannot run on this machine at all: `kernel.apparmor_restrict_unprivileged
_userns=1` makes `bwrap --unshare-net` fail, reproducible without any code from
either repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
325a2a0843
commit
bdee07a93b
@@ -226,6 +226,30 @@ function capabilityBadge(
|
||||
return { text: `활성 (${status.active})`, variant: "success" };
|
||||
}
|
||||
|
||||
/**
|
||||
* §3.5. The three states an operator has to be able to tell apart: shipped and
|
||||
* serving, shipped and switched off, and not in this build at all.
|
||||
*/
|
||||
const FEATURE_BADGE = Object.freeze({
|
||||
ACTIVE: Object.freeze({
|
||||
variant: "success" as const,
|
||||
text: "사용 중",
|
||||
description: "이 빌드에 설치되어 있고 런타임 설정이 끄지 않았습니다.",
|
||||
}),
|
||||
DISABLED_BY_CONFIG: Object.freeze({
|
||||
variant: "warning" as const,
|
||||
text: "설정으로 중지",
|
||||
description:
|
||||
"이 빌드에 포함되어 있으나 런타임 설정이 껐습니다. 재빌드 없이 다시 켤 수 있습니다.",
|
||||
}),
|
||||
NOT_INSTALLED: Object.freeze({
|
||||
variant: "neutral" as const,
|
||||
text: "미설치",
|
||||
description:
|
||||
"빌드 시 제품 매니페스트가 선택하지 않았습니다. 런타임 설정으로는 켤 수 없습니다.",
|
||||
}),
|
||||
});
|
||||
|
||||
function buildOperationRows(): readonly OperationRow[] {
|
||||
return Object.freeze(
|
||||
[...COMPOSED_CONTRACT_CONTRIBUTIONS.httpByOperationId.values()].map(
|
||||
@@ -273,6 +297,7 @@ export default function PlatformOverviewPage() {
|
||||
const routes = Object.values(ROUTE_REGISTRY);
|
||||
const operations = buildOperationRows();
|
||||
const capabilities = runtime.getCapabilitySnapshot();
|
||||
const features = runtime.getFeatureSnapshot();
|
||||
const activeCapabilityCount = capabilities.filter(
|
||||
(status) => status.active > 0,
|
||||
).length;
|
||||
@@ -479,6 +504,37 @@ export default function PlatformOverviewPage() {
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="gallery-section"
|
||||
aria-labelledby="platform-features-title"
|
||||
>
|
||||
<header className="gallery-section__header">
|
||||
<h2 id="platform-features-title">제품 기능</h2>
|
||||
<p>
|
||||
제품 매니페스트가 이 빌드에 어떤 기능이 설치되었는지 정하고, 런타임
|
||||
설정은 설치된 기능을 끌 수만 있습니다. 두 입력 모두 감산만 하므로
|
||||
설정 문서가 없는 기능을 켜 낼 수는 없습니다. 그래서 「빌드에서 빠진
|
||||
기능」과 「운영자가 끈 기능」이 여기서 구분됩니다.
|
||||
</p>
|
||||
</header>
|
||||
<div className="component-grid component-grid--two">
|
||||
{features.map((status) => {
|
||||
const badge = FEATURE_BADGE[status.state];
|
||||
return (
|
||||
<Card
|
||||
key={status.featureId}
|
||||
title={status.featureId}
|
||||
footer={<Badge variant={badge.variant}>{badge.text}</Badge>}
|
||||
>
|
||||
<p data-product-feature={status.featureId}>
|
||||
{badge.description}
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ const PLATFORM_KO_MESSAGES = {
|
||||
"route.invalid.title": "올바르지 않은 주소입니다.",
|
||||
"route.invalid.description": "주소의 경로 또는 검색 조건을 확인해 주세요.",
|
||||
"route.invalid.action": "안전한 탐색 링크를 사용해 주세요.",
|
||||
"route.disabledFeature.title": "현재 사용할 수 없는 기능입니다.",
|
||||
"route.disabledFeature.description":
|
||||
"이 기능은 배포 설정에서 중지되어 있습니다. 코드에는 포함되어 있으며 운영자가 다시 켤 수 있습니다.",
|
||||
"route.disabledFeature.action": "다른 탐색 링크를 사용해 주세요.",
|
||||
"route.auth.integration.title": "로그인 연동이 필요합니다.",
|
||||
"route.auth.integration.description":
|
||||
"외부 인증 소유자가 연결되면 이 보호 라우트를 사용할 수 있습니다.",
|
||||
@@ -233,6 +237,10 @@ const PLATFORM_EN_MESSAGES = {
|
||||
"route.invalid.title": "This address is invalid.",
|
||||
"route.invalid.description": "Check the path and search parameters.",
|
||||
"route.invalid.action": "Use a safe navigation link.",
|
||||
"route.disabledFeature.title": "This feature is not available right now.",
|
||||
"route.disabledFeature.description":
|
||||
"The deployment configuration has switched it off. It is still part of this build and an operator can switch it back on.",
|
||||
"route.disabledFeature.action": "Use another navigation link.",
|
||||
"route.auth.integration.title": "Sign-in integration is required.",
|
||||
"route.auth.integration.description":
|
||||
"This protected route is available after an external authentication owner is connected.",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { SessionState } from "../../application/ports/in/application-api.ts
|
||||
import { normalizeColorSchemePreference } from "../../application/policies/color-scheme.ts";
|
||||
import {
|
||||
NAVIGATION_ROUTES,
|
||||
ROUTE_FEATURE_OWNER,
|
||||
routePath,
|
||||
} from "../../features/installed-feature-contracts.ts";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
useLocale,
|
||||
type MessageKey,
|
||||
} from "../i18n/index.ts";
|
||||
import { useApplication } from "../providers/application-provider.tsx";
|
||||
import { useSession } from "../providers/session-provider.tsx";
|
||||
import { useTheme } from "../providers/theme-provider.tsx";
|
||||
|
||||
@@ -166,10 +168,17 @@ export function AppShell() {
|
||||
|
||||
function PrimaryNavigation({ id }: Readonly<{ id: string }>) {
|
||||
const { resolve, message } = useLocale();
|
||||
const { runtime } = useApplication();
|
||||
// §3.5. A feature the runtime document disabled does not advertise itself.
|
||||
// The router refuses its routes too, so this is presentation, not the switch.
|
||||
const routes = NAVIGATION_ROUTES.filter((definition) => {
|
||||
const owner = ROUTE_FEATURE_OWNER[definition.routeId];
|
||||
return owner === undefined || runtime.isFeatureActive(owner);
|
||||
});
|
||||
return (
|
||||
<nav id={id} aria-label={message("shell.primaryNavigation")}>
|
||||
<ul className="app-navigation">
|
||||
{NAVIGATION_ROUTES.map((definition) => (
|
||||
{routes.map((definition) => (
|
||||
<li key={definition.routeId}>
|
||||
<NavLink
|
||||
className={({ isActive }) =>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
|
||||
import {
|
||||
getRoute,
|
||||
ROUTE_FEATURE_OWNER,
|
||||
ROUTE_REGISTRY,
|
||||
} from "../../features/installed-feature-contracts.ts";
|
||||
import type { RouteDefinition } from "../../contracts/routes.ts";
|
||||
@@ -96,6 +97,26 @@ function InvalidRouteSurface({ code }: { code: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* §3.5. A route whose feature the runtime document switched off. It answers as
|
||||
* "not available" rather than rendering the feature or crashing, so disabling a
|
||||
* feature is a deployment action and not an outage.
|
||||
*/
|
||||
function DisabledFeatureSurface({ featureId }: { featureId: string }) {
|
||||
const { message } = useLocale();
|
||||
return (
|
||||
<section className="ui-page" data-surface="disabled-feature">
|
||||
<PageHeader
|
||||
title={message("route.disabledFeature.title")}
|
||||
description={message("route.disabledFeature.description")}
|
||||
/>
|
||||
<p data-disabled-feature={featureId}>
|
||||
{message("route.disabledFeature.action")}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteLifecycle({
|
||||
definition,
|
||||
buildId,
|
||||
@@ -245,11 +266,22 @@ function RegisteredRoute({
|
||||
buildId: string;
|
||||
}) {
|
||||
const definition = getRoute(routeId);
|
||||
const runtime = ROUTE_RUNTIME[routeId];
|
||||
const params = useParams();
|
||||
const [search] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const { diagnostics, recovery } = useApplication();
|
||||
const { diagnostics, runtime: platformRuntime, recovery } = useApplication();
|
||||
// §3.5. A feature the runtime document disabled is out of service, not
|
||||
// merely hidden: withdrawing it from navigation alone would leave a typed
|
||||
// deep link that still mounts it.
|
||||
const owner = ROUTE_FEATURE_OWNER[routeId];
|
||||
if (owner !== undefined && !platformRuntime.isFeatureActive(owner)) {
|
||||
return <DisabledFeatureSurface featureId={owner} />;
|
||||
}
|
||||
// The registry and the runtime table are composed from the same manifest, so
|
||||
// a route without a component means the two disagree — refuse rather than
|
||||
// crash the shell.
|
||||
const runtime = ROUTE_RUNTIME[routeId];
|
||||
if (!runtime) return <DisabledFeatureSurface featureId={owner ?? routeId} />;
|
||||
const parsed = parseRouteInput(routeId, params, search);
|
||||
if (!parsed.success) return <InvalidRouteSurface code={parsed.code} />;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user