diff --git a/artifacts/api-server/src/app.ts b/artifacts/api-server/src/app.ts index 585bdb50..e3b44841 100644 --- a/artifacts/api-server/src/app.ts +++ b/artifacts/api-server/src/app.ts @@ -64,7 +64,7 @@ export const sessionMiddleware = session({ if (process.env.NODE_ENV === "production") { throw new Error("SESSION_SECRET must be set in production"); } - return "teaboy-dev-secret-change-before-deploy"; + return "tx-dev-secret-change-before-deploy"; })(), resave: false, saveUninitialized: false, diff --git a/artifacts/api-server/tests/groups-crud.test.mjs b/artifacts/api-server/tests/groups-crud.test.mjs index 468cbeff..72981892 100644 --- a/artifacts/api-server/tests/groups-crud.test.mjs +++ b/artifacts/api-server/tests/groups-crud.test.mjs @@ -114,7 +114,7 @@ test("GET /api/groups requires admin (regular user gets 403)", async () => { assert.equal(res.status, 403); }); -test("admin lists default seed groups including Admins, TeaBoy, Everyone", async () => { +test("admin lists default seed groups including Admins, Tx, Everyone", async () => { const res = await fetch(`${API_BASE}/api/groups`, { headers: { Cookie: adminCookie }, }); @@ -122,7 +122,7 @@ test("admin lists default seed groups including Admins, TeaBoy, Everyone", async const list = await res.json(); const names = new Set(list.map((g) => g.name)); assert.ok(names.has("Admins")); - assert.ok(names.has("TeaBoy")); + assert.ok(names.has("Tx")); assert.ok(names.has("Everyone")); }); diff --git a/artifacts/teaboy-os/index.html b/artifacts/teaboy-os/index.html index 9ccce42f..9d9953a7 100644 --- a/artifacts/teaboy-os/index.html +++ b/artifacts/teaboy-os/index.html @@ -3,7 +3,7 @@ - TeaBoy OS + Tx OS diff --git a/artifacts/teaboy-os/src/components/clock.tsx b/artifacts/teaboy-os/src/components/clock.tsx index 02f0729e..fc6a09ae 100644 --- a/artifacts/teaboy-os/src/components/clock.tsx +++ b/artifacts/teaboy-os/src/components/clock.tsx @@ -339,8 +339,8 @@ export function AnalogClockWidget({ ); } -const HOME_CLOCK_STORAGE_KEY = "teaboy:home-clock-visible"; -const HOME_CLOCK_EVENT = "teaboy:home-clock-changed"; +const HOME_CLOCK_STORAGE_KEY = "tx:home-clock-visible"; +const HOME_CLOCK_EVENT = "tx:home-clock-changed"; function readHomeClockVisible(): boolean { if (typeof window === "undefined") return true; @@ -384,8 +384,8 @@ export function useHomeClockVisibility(): [boolean, (next: boolean) => void] { return [visible, setVisible]; } -const TOPBAR_CLOCK_STORAGE_KEY = "teaboy:topbar-clock-visible"; -const TOPBAR_CLOCK_EVENT = "teaboy:topbar-clock-changed"; +const TOPBAR_CLOCK_STORAGE_KEY = "tx:topbar-clock-visible"; +const TOPBAR_CLOCK_EVENT = "tx:topbar-clock-changed"; function readTopbarClockVisible(): boolean { if (typeof window === "undefined") return true; @@ -428,13 +428,13 @@ export function useTopbarClockVisibility(): [boolean, (next: boolean) => void] { return [visible, setVisible]; } -const HOME_CLOCK_SIZE_EVENT = "teaboy:home-clock-size-changed"; +const HOME_CLOCK_SIZE_EVENT = "tx:home-clock-size-changed"; export type HomeClockSize = "compact" | "large"; function homeClockSizeKey(userId: number | string | null | undefined): string { return userId == null - ? "teaboy:home-clock-size" - : `teaboy:home-clock-size:${userId}`; + ? "tx:home-clock-size" + : `tx:home-clock-size:${userId}`; } function readHomeClockSize(userId: number | string | null | undefined): HomeClockSize { @@ -473,12 +473,12 @@ export function useHomeClockSize( return [size, setSize]; } -const HOME_CLOCK_POS_EVENT = "teaboy:home-clock-pos-changed"; +const HOME_CLOCK_POS_EVENT = "tx:home-clock-pos-changed"; function homeClockPosKey(userId: number | string | null | undefined): string { return userId == null - ? "teaboy:home-clock-position" - : `teaboy:home-clock-position:${userId}`; + ? "tx:home-clock-position" + : `tx:home-clock-position:${userId}`; } function readHomeClockPosition(userId: number | string | null | undefined): number { diff --git a/artifacts/teaboy-os/src/hooks/use-app-settings.ts b/artifacts/teaboy-os/src/hooks/use-app-settings.ts index 970c39cd..23d66243 100644 --- a/artifacts/teaboy-os/src/hooks/use-app-settings.ts +++ b/artifacts/teaboy-os/src/hooks/use-app-settings.ts @@ -11,7 +11,7 @@ export function useAppName() { const { i18n } = useTranslation(); const { data } = useAppSettings(); const isAr = i18n.language === "ar"; - const fallback = isAr ? "نظام TeaBoy" : "TeaBoy OS"; + const fallback = isAr ? "نظام Tx" : "Tx OS"; const name = data ? (isAr ? data.siteNameAr : data.siteNameEn) : fallback; useEffect(() => { if (typeof document !== "undefined" && data) { diff --git a/artifacts/teaboy-os/src/i18n.ts b/artifacts/teaboy-os/src/i18n.ts index 2e1459c3..812cef6b 100644 --- a/artifacts/teaboy-os/src/i18n.ts +++ b/artifacts/teaboy-os/src/i18n.ts @@ -4,7 +4,7 @@ import ar from './locales/ar.json'; import en from './locales/en.json'; // Initialize i18next -const savedLanguage = localStorage.getItem('teaboy-lang') || 'ar'; +const savedLanguage = localStorage.getItem('tx-lang') || 'ar'; i18n .use(initReactI18next) @@ -26,7 +26,7 @@ document.documentElement.lang = savedLanguage; i18n.on('languageChanged', (lng) => { document.documentElement.dir = lng === 'ar' ? 'rtl' : 'ltr'; document.documentElement.lang = lng; - localStorage.setItem('teaboy-lang', lng); + localStorage.setItem('tx-lang', lng); }); export default i18n; diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 3aa96719..d071bac3 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -488,7 +488,7 @@ "error": "حدث خطأ", "success": "تمت العملية بنجاح", "language": "English", - "appName": "نظام TeaBoy" + "appName": "نظام Tx" }, "notes": { "title": "الملاحظات", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 30472b68..57789296 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -485,7 +485,7 @@ "error": "An error occurred", "success": "Success", "language": "العربية", - "appName": "TeaBoy OS" + "appName": "Tx OS" }, "notes": { "title": "Notes", diff --git a/artifacts/teaboy-os/src/pages/chat.tsx b/artifacts/teaboy-os/src/pages/chat.tsx index ee570e9d..6c12d097 100644 --- a/artifacts/teaboy-os/src/pages/chat.tsx +++ b/artifacts/teaboy-os/src/pages/chat.tsx @@ -253,7 +253,7 @@ export default function ChatPage() { // arrive in the conversation before the user next opens chat. useEffect(() => { if (!user || !conversations) return; - const ackKey = `teaboy:admin-known:${user.id}`; + const ackKey = `tx:admin-known:${user.id}`; let ack: number[] = []; try { ack = JSON.parse(localStorage.getItem(ackKey) ?? "[]"); diff --git a/artifacts/teaboy-os/tests/home-clock-persistence.spec.mjs b/artifacts/teaboy-os/tests/home-clock-persistence.spec.mjs index f0941023..44cace59 100644 --- a/artifacts/teaboy-os/tests/home-clock-persistence.spec.mjs +++ b/artifacts/teaboy-os/tests/home-clock-persistence.spec.mjs @@ -1,7 +1,7 @@ // UI test verifying that the home-page clock widget keeps its grid position // and compact/large size after a full page reload, and that the changes are // persisted to localStorage under the documented keys -// `teaboy:home-clock-position` / `teaboy:home-clock-size` (or their per-user +// `tx:home-clock-position` / `tx:home-clock-size` (or their per-user // variants once a user is logged in). // // This is the recurring automated counterpart to the manual e2e check @@ -95,9 +95,9 @@ async function readClockStorage(page, userId) { return page.evaluate((uid) => { return { position: window.localStorage.getItem( - `teaboy:home-clock-position:${uid}`, + `tx:home-clock-position:${uid}`, ), - size: window.localStorage.getItem(`teaboy:home-clock-size:${uid}`), + size: window.localStorage.getItem(`tx:home-clock-size:${uid}`), }; }, userId); } diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 5947c93b..a524f7fb 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -2,7 +2,7 @@ * Generated by orval v8.5.3 🍺 * Do not edit manually. * Api - * TeaBoy OS API specification + * Tx OS API specification * OpenAPI spec version: 0.1.0 */ export interface HealthStatus { diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index 67af3f1a..8e4e2d68 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -2,7 +2,7 @@ * Generated by orval v8.5.3 🍺 * Do not edit manually. * Api - * TeaBoy OS API specification + * Tx OS API specification * OpenAPI spec version: 0.1.0 */ import { useMutation, useQuery } from "@tanstack/react-query"; diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index cc290e02..9f92faec 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -3,7 +3,7 @@ info: # Do not change the title, if the title changes, the import paths will be broken title: Api version: 0.1.0 - description: TeaBoy OS API specification + description: Tx OS API specification servers: - url: /api description: Base API path diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 959932c4..2f23f30d 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -2,7 +2,7 @@ * Generated by orval v8.5.3 🍺 * Do not edit manually. * Api - * TeaBoy OS API specification + * Tx OS API specification * OpenAPI spec version: 0.1.0 */ import * as zod from "zod"; diff --git a/lib/db/src/schema/settings.ts b/lib/db/src/schema/settings.ts index df51a5f7..e7b43562 100644 --- a/lib/db/src/schema/settings.ts +++ b/lib/db/src/schema/settings.ts @@ -4,8 +4,8 @@ import { z } from "zod/v4"; export const appSettingsTable = pgTable("app_settings", { id: integer("id").primaryKey().default(1), - siteNameAr: varchar("site_name_ar", { length: 200 }).notNull().default("نظام TeaBoy"), - siteNameEn: varchar("site_name_en", { length: 200 }).notNull().default("TeaBoy OS"), + siteNameAr: varchar("site_name_ar", { length: 200 }).notNull().default("نظام Tx"), + siteNameEn: varchar("site_name_en", { length: 200 }).notNull().default("Tx OS"), registrationOpen: boolean("registration_open").notNull().default(true), footerTextAr: varchar("footer_text_ar", { length: 300 }).notNull().default("جميع الحقوق محفوظة."), footerTextEn: varchar("footer_text_en", { length: 300 }).notNull().default("All rights reserved."), diff --git a/replit.md b/replit.md index 8f8b4653..7ed2343d 100644 --- a/replit.md +++ b/replit.md @@ -1,8 +1,8 @@ -# TeaBoy OS +# Tx OS ## Overview -**TeaBoy OS** — a bilingual (Arabic/English, RTL/LTR) full-stack internal web platform styled as an OS-like interface with glassmorphism aesthetics. Built as a pnpm monorepo. +**Tx OS** — a bilingual (Arabic/English, RTL/LTR) full-stack internal web platform styled as an OS-like interface with glassmorphism aesthetics. Built as a pnpm monorepo. ## Architecture @@ -11,6 +11,7 @@ ├── artifacts/ │ ├── api-server/ Express 5 backend (port 8080) │ └── teaboy-os/ React + Vite frontend (path: /, port dynamic) +│ (legacy folder name; product brand is "Tx OS") ├── lib/ │ ├── db/ Drizzle ORM + PostgreSQL schema │ ├── api-spec/ OpenAPI spec + Orval codegen @@ -66,3 +67,4 @@ - Socket.IO server path: `/api/socket.io` - Frontend connects to Socket.IO via same-origin proxy (no separate URL needed) - i18n locale files: `artifacts/teaboy-os/src/locales/ar.json` and `en.json` +- Default receivers group is named **Tx** (was renamed from a legacy "TeaBoy"); a one-time migration in the seed script renames any pre-existing legacy group on next run. diff --git a/scripts/post-merge.sh b/scripts/post-merge.sh index be2dbc93..0a97617f 100644 --- a/scripts/post-merge.sh +++ b/scripts/post-merge.sh @@ -3,6 +3,6 @@ set -e pnpm install --frozen-lockfile pnpm --filter db run push-force # Idempotent: ensure the default groups exist and existing users are mapped -# (Admins, TeaBoy, Everyone). Safe to run repeatedly. +# (Admins, Tx, Everyone). Safe to run repeatedly. pnpm --filter scripts run seed pnpm --filter @workspace/teaboy-os exec playwright install chromium diff --git a/scripts/src/seed.ts b/scripts/src/seed.ts index 66ad0704..a332b56f 100644 --- a/scripts/src/seed.ts +++ b/scripts/src/seed.ts @@ -102,7 +102,7 @@ async function main() { .insert(usersTable) .values({ username: "admin", - email: "admin@teaboy.local", + email: "admin@tx.local", passwordHash: adminHash, displayNameAr: "مدير النظام", displayNameEn: "System Admin", @@ -118,7 +118,7 @@ async function main() { .insert(usersTable) .values({ username: "ahmed", - email: "ahmed@teaboy.local", + email: "ahmed@tx.local", passwordHash: userHash, displayNameAr: "أحمد محمد", displayNameEn: "Ahmed Mohammed", @@ -369,7 +369,7 @@ async function main() { isSystem: 1, }, { - name: "TeaBoy", + name: "Tx", descriptionAr: "فريق إعداد وتقديم الطلبات", descriptionEn: "Team that prepares and delivers orders", isSystem: 1, @@ -383,9 +383,21 @@ async function main() { ]) .onConflictDoNothing(); + // One-time migration: rename legacy "TeaBoy" group to "Tx" if it still exists + // and the new "Tx" group hasn't been created yet. + const existingGroups = await db.select().from(groupsTable); + const legacy = existingGroups.find((g) => g.name === "TeaBoy"); + const newOne = existingGroups.find((g) => g.name === "Tx"); + if (legacy && !newOne) { + await db + .update(groupsTable) + .set({ name: "Tx" }) + .where(eq(groupsTable.id, legacy.id)); + } + const allGroups = await db.select().from(groupsTable); const adminsGroup = allGroups.find((g) => g.name === "Admins"); - const teaboyGroup = allGroups.find((g) => g.name === "TeaBoy"); + const txGroup = allGroups.find((g) => g.name === "Tx"); const everyoneGroup = allGroups.find((g) => g.name === "Everyone"); const allRolesNow = await db.select().from(rolesTable); @@ -399,10 +411,10 @@ async function main() { .values({ groupId: adminsGroup.id, roleId: adminRoleNow.id }) .onConflictDoNothing(); } - if (teaboyGroup && orderReceiverNow) { + if (txGroup && orderReceiverNow) { await db .insert(groupRolesTable) - .values({ groupId: teaboyGroup.id, roleId: orderReceiverNow.id }) + .values({ groupId: txGroup.id, roleId: orderReceiverNow.id }) .onConflictDoNothing(); } @@ -414,13 +426,13 @@ async function main() { .values(allAppsNow.map((a) => ({ groupId: adminsGroup.id, appId: a.id }))) .onConflictDoNothing(); } - // TeaBoy gets the services app (which is where receivers see incoming orders) - if (teaboyGroup) { + // The Tx receivers group gets the services app (where receivers see incoming orders) + if (txGroup) { const servicesApp = allAppsNow.find((a) => a.slug === "services"); if (servicesApp) { await db .insert(groupAppsTable) - .values({ groupId: teaboyGroup.id, appId: servicesApp.id }) + .values({ groupId: txGroup.id, appId: servicesApp.id }) .onConflictDoNothing(); } } @@ -447,7 +459,7 @@ async function main() { } } - if (teaboyGroup && orderReceiverNow) { + if (txGroup && orderReceiverNow) { const receiverRows = await db .select({ userId: userRolesTable.userId }) .from(userRolesTable) @@ -455,7 +467,7 @@ async function main() { if (receiverRows.length > 0) { await db .insert(userGroupsTable) - .values(receiverRows.map((r) => ({ userId: r.userId, groupId: teaboyGroup.id }))) + .values(receiverRows.map((r) => ({ userId: r.userId, groupId: txGroup.id }))) .onConflictDoNothing(); } }