From 84621bb628df3d942f6fe3f27289bf0b36d2b4c5 Mon Sep 17 00:00:00 2001 From: mahmutKaya <33642821+mahmutkaya@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:43:34 +0200 Subject: [PATCH 1/2] perf(fleet): scope /admin/fleet reads to registry tenant slugs (Gemini #71 follow-up) (#72) Filter fleetDevice/fleetSummary findMany by the registry's active tenant slugs so orphaned/decommissioned rows aren't loaded as the fleet grows. No-op at the current single-tenant scale; the deferred optimization from the #71 release review. Co-authored-by: Claude Opus 4.8 --- app/(control)/admin/fleet/page.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/(control)/admin/fleet/page.tsx b/app/(control)/admin/fleet/page.tsx index 288d750..2552953 100644 --- a/app/(control)/admin/fleet/page.tsx +++ b/app/(control)/admin/fleet/page.tsx @@ -139,9 +139,15 @@ export default async function AdminFleetPage() { ); } + // Scope the reads to the tenants we'll actually render (the registry), so orphaned/decommissioned + // rows don't get loaded as the fleet grows (Gemini review on #71). + const tenantSlugs = registry.tenants.map((t) => t.slug); const [devices, summaries] = await Promise.all([ - db.fleetDevice.findMany({ orderBy: [{ tenantSlug: "asc" }, { label: "asc" }] }), - db.fleetSummary.findMany(), + db.fleetDevice.findMany({ + where: { tenantSlug: { in: tenantSlugs } }, + orderBy: [{ tenantSlug: "asc" }, { label: "asc" }], + }), + db.fleetSummary.findMany({ where: { tenantSlug: { in: tenantSlugs } } }), ]); const devicesBySlug = new Map(); From 4d3f50f1d6b82e062ee956f0e742ebd1f9fdcd82 Mon Sep 17 00:00:00 2001 From: mahmutKaya <33642821+mahmutkaya@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:37:00 +0200 Subject: [PATCH 2/2] feat(brand): SofraPiwas onion logo across header, favicon & OG (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(brand): SofraPiwas onion logo across header, favicon & OG Replace the placeholder two-circle plate with the SofraPiwas onion mark (traced from the owner's Stitch art) plus a "SofraPiwas" wordmark set in Caveat — the existing `font-hand` — across the marketing header and the control-plane shell (shared BrandMark component; mark is decorative, the wordmark carries the accessible name). Add favicon.svg (onion, replaces the two-circle plate), apple-touch-icon and a 1200x630 OG/social image; wire openGraph + twitter cards via a shared OG_IMAGE so every marketing page carries the logo. Co-Authored-By: Claude Opus 4.8 * feat(brand): rename product "Sofra" → "SofraPiwas" across the site Follows the logo rollout — the product is now presented as "SofraPiwas" everywhere: marketing header + copy, control-plane, auth pages, meta titles, openGraph siteName, JSON-LD organization name, transactional email from/subjects, and llms.txt. Done as a word-boundary rename (\bSofra\b → SofraPiwas), which deliberately preserves the lowercase "sofra" wordplay — the Turkish word for a laid table ("A «sofra» is a table laid for guests", "We lay your sofra") — and the camelCase i18n keys (fitSofra, managedBySofra) and Turkish inflected forms. The etymology one-liners (en/fr/de/nl meta.description + llms.txt) were reworded to reference "sofra" accurately rather than claim the full name means "a laid table"; tr/ar already phrase it with the word natively. i18n key parity unchanged (587 leaf strings × 6 locales); tsc + eslint clean; verified in-browser (marketing hero + footer + partner login). Co-Authored-By: Claude Opus 4.8 * chore(deps): bump brace-expansion to clear GHSA-3jxr-9vmj-r5cp (high) Newly-published high-severity advisory (ReDoS/DoS via exponential {} group expansion) in the transitive brace-expansion dep, failing the required npm-audit + OSV checks. Lockfile-only bump via npm 10 (keeps lockfileVersion 3 / node-22 CI compatible). Unrelated to the branding change; folded in to keep this PR green. Co-Authored-By: Claude Opus 4.8 * test(brand): update unit tests for the SofraPiwas rename email-templates + seo unit tests asserted the old \"Sofra\" brand string (brand shell + openGraph siteName); update to \"SofraPiwas\" to match the renamed source. Same word-boundary rename applied to tests/. 125/125 pass. Co-Authored-By: Claude Opus 4.8 * fix(brand): address Gemini review — explicit img dims + reuse OG_IMAGE - BrandMark gets explicit width/height (452x501, the mark.svg intrinsic box) to reserve aspect-ratio and avoid CLS; CSS (h-10 w-auto) still controls the rendered size. - twitter.images now passes the shared OG_IMAGE object directly (locale layout + marketingPageMetadata) instead of reconstructing {url, alt}. Co-Authored-By: Claude Opus 4.8 * fix(brand): mark BrandMark props read-only (Sonar S6759) Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- app/(control)/billing/thanks/page.tsx | 2 +- app/(control)/forgot/page.tsx | 2 +- app/(control)/invite/[token]/page.tsx | 2 +- app/(control)/layout.tsx | 4 +- app/(control)/login/page.tsx | 2 +- app/(control)/reset/[token]/page.tsx | 2 +- app/[locale]/case/rumi/page.tsx | 4 +- app/[locale]/compare/gloriafood/page.tsx | 2 +- app/[locale]/layout.tsx | 13 +- app/api/partner/apply/route.ts | 2 +- app/api/signup/route.ts | 2 +- app/api/waitlist/route.ts | 2 +- components/BrandMark.tsx | 21 ++++ components/Footer.tsx | 2 +- components/Header.tsx | 9 +- components/JsonLd.tsx | 8 +- components/control/ClientPipelineControls.tsx | 2 +- components/control/ControlShell.tsx | 9 +- lib/actions/admin-actions.ts | 4 +- lib/actions/auth-actions.ts | 4 +- lib/actions/onboarding-actions.ts | 8 +- lib/actions/partner-actions.ts | 2 +- lib/billing.ts | 2 +- lib/email-templates.ts | 4 +- lib/email.ts | 2 +- lib/fonts.ts | 2 +- lib/format.ts | 2 +- lib/mollie.ts | 2 +- lib/seo.ts | 23 +++- messages/ar.json | 12 +- messages/de.json | 114 +++++++++--------- messages/en.json | 108 ++++++++--------- messages/fr.json | 108 ++++++++--------- messages/nl.json | 108 ++++++++--------- messages/tr.json | 114 +++++++++--------- package-lock.json | 6 +- public/apple-touch-icon.png | Bin 0 -> 18960 bytes public/favicon.svg | 22 ++-- public/llms.txt | 16 +-- public/mark.svg | 85 +++++++++++++ public/og-image.png | Bin 0 -> 122455 bytes tests/unit/email-templates.test.ts | 4 +- tests/unit/seo.test.ts | 2 +- tests/unit/validation.test.ts | 12 +- 44 files changed, 503 insertions(+), 353 deletions(-) create mode 100644 components/BrandMark.tsx create mode 100644 public/apple-touch-icon.png create mode 100644 public/mark.svg create mode 100644 public/og-image.png diff --git a/app/(control)/billing/thanks/page.tsx b/app/(control)/billing/thanks/page.tsx index b658b3c..58e0511 100644 --- a/app/(control)/billing/thanks/page.tsx +++ b/app/(control)/billing/thanks/page.tsx @@ -12,7 +12,7 @@ export default async function BillingThanksPage() { return (
- Sofra + SofraPiwas

{t("title")}

{t("body")}

diff --git a/app/(control)/forgot/page.tsx b/app/(control)/forgot/page.tsx index b7649e6..0303c5d 100644 --- a/app/(control)/forgot/page.tsx +++ b/app/(control)/forgot/page.tsx @@ -10,7 +10,7 @@ export default async function ForgotPage() { return (
- Sofra + SofraPiwas

{t("forgotTitle")}

{t("forgotIntro")}

diff --git a/app/(control)/invite/[token]/page.tsx b/app/(control)/invite/[token]/page.tsx index cff324b..bb3040a 100644 --- a/app/(control)/invite/[token]/page.tsx +++ b/app/(control)/invite/[token]/page.tsx @@ -17,7 +17,7 @@ export default async function InvitePage({ return (
- Sofra + SofraPiwas

{t("inviteTitle")}

{valid ? ( diff --git a/app/(control)/layout.tsx b/app/(control)/layout.tsx index 3217050..91f7b8a 100644 --- a/app/(control)/layout.tsx +++ b/app/(control)/layout.tsx @@ -11,9 +11,9 @@ import { controlLocale } from "@/lib/control-locale"; import { fontClassNames, themeInitScript } from "@/lib/fonts"; export const metadata: Metadata = { - title: "Sofra — Partner area", + title: "SofraPiwas — Partner area", robots: { index: false, follow: false }, - icons: { icon: "/favicon.svg" }, + icons: { icon: "/favicon.svg", apple: "/apple-touch-icon.png" }, }; export default async function ControlLayout({ children }: { children: React.ReactNode }) { diff --git a/app/(control)/login/page.tsx b/app/(control)/login/page.tsx index 8a48c46..2e91796 100644 --- a/app/(control)/login/page.tsx +++ b/app/(control)/login/page.tsx @@ -22,7 +22,7 @@ export default async function LoginPage({ return (
- Sofra + SofraPiwas

{t("loginTitle")}

diff --git a/app/(control)/reset/[token]/page.tsx b/app/(control)/reset/[token]/page.tsx index 360e5b8..f55f1ea 100644 --- a/app/(control)/reset/[token]/page.tsx +++ b/app/(control)/reset/[token]/page.tsx @@ -17,7 +17,7 @@ export default async function ResetPage({ return (

- Sofra + SofraPiwas

{t("resetTitle")}

{valid ? ( diff --git a/app/[locale]/case/rumi/page.tsx b/app/[locale]/case/rumi/page.tsx index 83d741f..7d9e87c 100644 --- a/app/[locale]/case/rumi/page.tsx +++ b/app/[locale]/case/rumi/page.tsx @@ -53,8 +53,8 @@ export default async function RumiCaseStudyPage({ dateModified: PUBLISHED, inLanguage: locale, mainEntityOfPage: `${SITE_URL}/${locale}/case/rumi`, - author: { "@type": "Organization", name: "Sofra", url: SITE_URL }, - publisher: { "@type": "Organization", name: "Sofra", url: SITE_URL }, + author: { "@type": "Organization", name: "SofraPiwas", url: SITE_URL }, + publisher: { "@type": "Organization", name: "SofraPiwas", url: SITE_URL }, about: { "@type": "Restaurant", name: "RUMI Restaurant", diff --git a/app/[locale]/compare/gloriafood/page.tsx b/app/[locale]/compare/gloriafood/page.tsx index ef30959..4303e4a 100644 --- a/app/[locale]/compare/gloriafood/page.tsx +++ b/app/[locale]/compare/gloriafood/page.tsx @@ -15,7 +15,7 @@ import { } from "@/components/compare-gloriafood-data"; import { SITE_URL, marketingPageMetadata } from "@/lib/seo"; -// Honest, dated Sofra-vs-GloriaFood comparison (AEO plan §2 priority 2). +// Honest, dated SofraPiwas-vs-GloriaFood comparison (AEO plan §2 priority 2). // Every GloriaFood claim is sourced (see compare-gloriafood-data.ts) — // re-verify the sources before editing any factual cell. diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 5ee1bf0..c2f9224 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -4,7 +4,7 @@ import { NextIntlClientProvider, hasLocale } from "next-intl"; import { setRequestLocale, getTranslations } from "next-intl/server"; import "../globals.css"; import { routing } from "@/i18n/routing"; -import { SITE_URL, pageAlternates } from "@/lib/seo"; +import { SITE_URL, pageAlternates, OG_IMAGE } from "@/lib/seo"; import { fontClassNames, themeInitScript } from "@/lib/fonts"; import ThemeSync from "@/components/ThemeSync"; @@ -24,14 +24,21 @@ export async function generateMetadata({ metadataBase: new URL(SITE_URL), title: t("title"), description: t("description"), - icons: { icon: "/favicon.svg" }, + icons: { icon: "/favicon.svg", apple: "/apple-touch-icon.png" }, alternates: pageAlternates(locale, ""), openGraph: { title: t("title"), description: t("description"), - siteName: "Sofra", + siteName: "SofraPiwas", locale, type: "website", + images: [OG_IMAGE], + }, + twitter: { + card: "summary_large_image", + title: t("title"), + description: t("description"), + images: [OG_IMAGE], }, robots: { index: true, follow: true }, }; diff --git a/app/api/partner/apply/route.ts b/app/api/partner/apply/route.ts index e037dae..aa26151 100644 --- a/app/api/partner/apply/route.ts +++ b/app/api/partner/apply/route.ts @@ -38,7 +38,7 @@ export async function POST(request: Request) { await sendEmail({ to, replyTo: data.email, - subject: `Sofra — Partner application: ${data.name}`, + subject: `SofraPiwas — Partner application: ${data.name}`, html: craftEmail({ kicker: "Partner program", title: "New partner application", diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts index ae6d3a1..fd36a89 100644 --- a/app/api/signup/route.ts +++ b/app/api/signup/route.ts @@ -44,7 +44,7 @@ export async function POST(request: Request) { await sendEmail({ to, replyTo: data.email, - subject: `Sofra — New signup: ${data.restaurantName}`, + subject: `SofraPiwas — New signup: ${data.restaurantName}`, html: craftEmail({ kicker: "Signups", title: "New restaurant signup", diff --git a/app/api/waitlist/route.ts b/app/api/waitlist/route.ts index 3861be1..8621eab 100644 --- a/app/api/waitlist/route.ts +++ b/app/api/waitlist/route.ts @@ -54,7 +54,7 @@ export async function POST(request: Request) { const { sent } = await sendEmail({ to, replyTo: email, - subject: `Sofra — ${meta.subject}: ${restaurant}`, + subject: `SofraPiwas — ${meta.subject}: ${restaurant}`, html: craftEmail({ kicker: meta.kicker, title: meta.title, diff --git a/components/BrandMark.tsx b/components/BrandMark.tsx new file mode 100644 index 0000000..e4d2aac --- /dev/null +++ b/components/BrandMark.tsx @@ -0,0 +1,21 @@ +type BrandMarkProps = { className?: string }; + +/** + * The SofraPiwas onion mark (piwas). Decorative in the brand lockup — the + * adjacent "SofraPiwas" wordmark carries the accessible name — so it's + * `aria-hidden` with an empty alt. A static SVG served from /public; next/image + * would add no value (no optimization for inline vector) so a plain is used. + */ +export default function BrandMark({ className }: Readonly) { + return ( + // eslint-disable-next-line @next/next/no-img-element -- static SVG logo, no next/image optimization + + ); +} diff --git a/components/Footer.tsx b/components/Footer.tsx index 100c9f9..b8e23cf 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -10,7 +10,7 @@ export default function Footer() {