Skip to content

Commit f93f643

Browse files
committed
refactor: remove dead code and fold duplicated logic across the stack
Whole-repo over-engineering pass (~-926 lines, 52 files). Backend: - drop redundant selection_enabled column (modules JSON is now the single source of truth); add migration 0011, derive the API field from modules - fold the fixed-window rateLimit middleware into the shared sliding-window checkRateLimit - dedup attachDb into requireDb; inline warmCatalogAfterMutation - delete empty db/client.ts and the unused middleware barrel Schemas: - delete unused Allergen/Membership/ExtrasType/PublicationState/AnalyticsPeriod enums and ~30 dead type aliases and response schemas - drop the dead legacy.selectionEnabled fallback from normalizeModulesConfig Web: - collapse the never-wired dual-display path (getContentDisplayText/.secondary/ nameSecondary) down to getLocalizedContentValue - delete generateColorVariables/hexToHSL, the stores barrel, unused selector hooks, MENU_ICON_LABELS and the LOCALE_SHORT_CODES map - share one formatPrice and one sanitizeRichText helper - replace the chatActionsStore.dispatch switch with direct setters Chat worker: - remove the variant/extra pipeline that D1 never populated, its types and queries; align the get_item_detail tool description - drop the dev-only /chat/debug endpoint and the buildWorkersAIPrompt wrapper
1 parent 0fecf2e commit f93f643

53 files changed

Lines changed: 119 additions & 1041 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- selection_enabled was a legacy mirror of modules.ordering.enabled (added in
2+
-- 0007, folded into the JSON modules column in 0010). It is now the single
3+
-- source of truth, so the column is redundant.
4+
ALTER TABLE settings DROP COLUMN selection_enabled;

backend/src/__tests__/admin-crud.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createTestDb, makeDbEnv, seedSettings, seedMenu, seedCategory, seedEntr
55
beforeAll(() => installJwksMock());
66

77
const ADMIN_UID = 'admin-1';
8-
type SettingsRow = { name: string; payoff: string; ai_chat_enabled: number; ai_voice_enabled: number; selection_enabled: number };
8+
type SettingsRow = { name: string; payoff: string; ai_chat_enabled: number; ai_voice_enabled: number };
99
type PublicationRow = { publication_state: string };
1010
type CategoryRow = { name: string };
1111
type EntryRow = { name: string; price: number; hidden: number; out_of_stock: number; category_id: string };
@@ -45,12 +45,12 @@ describe('PUT /admin/settings', () => {
4545
body: { name: 'Updated Restaurant', payoff: 'Updated tagline', aiChatEnabled: true, aiVoiceEnabled: true, selectionEnabled: true },
4646
});
4747
expect(res.status).toBe(200);
48-
const row = db.raw.prepare('SELECT name, payoff, ai_chat_enabled, ai_voice_enabled, selection_enabled FROM settings WHERE id = 1').get() as SettingsRow;
48+
const row = db.raw.prepare('SELECT name, payoff, ai_chat_enabled, ai_voice_enabled, modules FROM settings WHERE id = 1').get() as SettingsRow & { modules: string };
4949
expect(row.name).toBe('Updated Restaurant');
5050
expect(row.payoff).toBe('Updated tagline');
5151
expect(row.ai_chat_enabled).toBe(1);
5252
expect(row.ai_voice_enabled).toBe(1);
53-
expect(row.selection_enabled).toBe(1);
53+
expect(JSON.parse(row.modules).ordering.enabled).toBe(true);
5454
});
5555

5656
it('turns voice dictation off when Tony chat is disabled', async () => {

backend/src/db/client.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

backend/src/db/schema.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export const settings = sqliteTable('settings', {
5858
chatAgentPrompt: text('chat_agent_prompt'),
5959
aiChatEnabled: integer('ai_chat_enabled', { mode: 'boolean' }).default(false).notNull(),
6060
aiVoiceEnabled: integer('ai_voice_enabled', { mode: 'boolean' }).default(false).notNull(),
61-
selectionEnabled: integer('selection_enabled', { mode: 'boolean' }).default(false).notNull(),
6261
modules: jsonColumn<Record<string, unknown> | null>('modules'),
6362
primaryLocale: text('primary_locale').default('it').notNull(),
6463
enabledLocales: jsonColumn<string[] | null>('enabled_locales'),

backend/src/middleware/admin-guard.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { createMiddleware } from 'hono/factory';
2-
import type { Env, RuntimeConfig } from '../types';
2+
import type { Env } from '../types';
33
import type { AuthUser } from './auth';
44
import { createDb } from '../db/index';
55
import { isDemoMode } from '../lib/demo';
66

77
type AdminBindings = {
88
Bindings: Env;
99
Variables: {
10-
config: RuntimeConfig;
1110
user: AuthUser;
1211
db: ReturnType<typeof createDb>;
1312
};
@@ -19,19 +18,6 @@ function parseAdminEmails(env: Env): Set<string> {
1918
return new Set(raw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
2019
}
2120

22-
/**
23-
* Attaches a Drizzle DB client to the request context.
24-
* Must run before any route that touches the database.
25-
*/
26-
export const attachDb = createMiddleware<AdminBindings>(async (c, next) => {
27-
const db = createDb(c.env);
28-
if (!db) {
29-
return c.json({ error: 'Database not configured' }, 503);
30-
}
31-
c.set('db', db);
32-
await next();
33-
});
34-
3521
/**
3622
* Allows the request only if the authenticated user's email is in the
3723
* comma-separated `ADMIN_EMAILS` env var. Email comparison is case-insensitive.

backend/src/middleware/index.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

backend/src/middleware/logging.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createMiddleware } from 'hono/factory';
22
import type { AppBindings } from '../types';
3+
import { checkRateLimit } from '../lib/rate-limit';
34

45
/**
56
* Request logging middleware.
@@ -29,37 +30,14 @@ export const requestLogger = createMiddleware<AppBindings>(async (c, next) => {
2930
});
3031

3132
/**
32-
* Simple rate limiter using Cloudflare's cf-connecting-ip.
33-
* Tracks request counts in a Map (per-isolate, not distributed).
33+
* Per-IP rate limiter using Cloudflare's cf-connecting-ip.
34+
* Delegates to the shared sliding-window limiter in lib/rate-limit.
3435
*/
35-
// ponytail: per-isolate counter, not distributed; effective limit scales with the
36-
// number of live isolates. Upgrade path: Cloudflare Rate Limiting rules, a Durable
37-
// Object, or Workers KV for a global limit.
38-
const requestCounts = new Map<string, { count: number; resetAt: number }>();
39-
4036
export function rateLimit(maxRequests: number, windowMs: number) {
4137
return createMiddleware<AppBindings>(async (c, next) => {
4238
const ip = c.req.header('cf-connecting-ip') || 'unknown';
43-
const now = Date.now();
44-
45-
let entry = requestCounts.get(ip);
46-
if (!entry || now > entry.resetAt) {
47-
entry = { count: 0, resetAt: now + windowMs };
48-
requestCounts.set(ip, entry);
49-
}
50-
51-
entry.count++;
52-
53-
if (entry.count > maxRequests) {
54-
return c.json(
55-
{ error: 'Too many requests', retryAfter: Math.ceil((entry.resetAt - now) / 1000) },
56-
429,
57-
);
58-
}
59-
60-
c.header('X-RateLimit-Limit', String(maxRequests));
61-
c.header('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
62-
39+
const limited = checkRateLimit(`ip:${ip}`, maxRequests, windowMs);
40+
if (limited) return limited;
6341
await next();
6442
});
6543
}

backend/src/routes/admin.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Hono, type Context } from 'hono';
22
import { eq, and, gte, lt, asc, desc, count, inArray, sql } from 'drizzle-orm';
33
import { requireAuth } from '../middleware/auth';
4-
import { attachDb, requireAdmin } from '../middleware/admin-guard';
4+
import { requireAdmin } from '../middleware/admin-guard';
5+
import { requireDb } from '../middleware/db';
56
import * as schema from '../db/schema';
67
import { computeLeaderboardMovement, VALID_PERIODS, periodToMs, computeWindows } from '../lib/analytics';
7-
import { buildCatalogFromDb, warmCatalogAfterMutation } from './catalog';
8+
import { buildCatalogFromDb, refreshCatalogArtifacts } from './catalog';
89
import { parseBody } from '../lib/validate';
910
import { validateImage } from '../lib/image';
1011
import { checkRateLimit } from '../lib/rate-limit';
@@ -33,7 +34,7 @@ import type { DbInstance } from '../db';
3334
// All admin routes require auth + db + admin gate.
3435
const admin = new Hono<AppBindings>();
3536

36-
const base = [requireAuth, attachDb, requireAdmin] as const;
37+
const base = [requireAuth, requireDb, requireAdmin] as const;
3738

3839

3940
admin.post('/demo/reset', ...base, async (c) => {
@@ -62,7 +63,7 @@ admin.get('/settings', ...base, async (c) => {
6263
chatAgentPrompt: schema.settings.chatAgentPrompt,
6364
aiChatEnabled: schema.settings.aiChatEnabled,
6465
aiVoiceEnabled: schema.settings.aiVoiceEnabled,
65-
selectionEnabled: schema.settings.selectionEnabled,
66+
modules: schema.settings.modules,
6667
promotionAlert: schema.settings.promotionAlert,
6768
publicationState: schema.settings.publicationState,
6869
primaryLocale: schema.settings.primaryLocale,
@@ -79,7 +80,7 @@ admin.get('/settings', ...base, async (c) => {
7980
chatAgentPrompt: row.chatAgentPrompt ?? '',
8081
aiChatEnabled: row.aiChatEnabled,
8182
aiVoiceEnabled: row.aiChatEnabled && row.aiVoiceEnabled,
82-
selectionEnabled: row.selectionEnabled,
83+
selectionEnabled: normalizeModulesConfig(row.modules, row).ordering.enabled,
8384
promotionAlert: row.promotionAlert ?? null,
8485
publicationState: row.publicationState,
8586
primaryLocale: row.primaryLocale,
@@ -95,7 +96,6 @@ admin.get('/modules', ...base, async (c) => {
9596
const [row] = await c.get('db')
9697
.select({
9798
modules: schema.settings.modules,
98-
selectionEnabled: schema.settings.selectionEnabled,
9999
aiChatEnabled: schema.settings.aiChatEnabled,
100100
aiVoiceEnabled: schema.settings.aiVoiceEnabled,
101101
})
@@ -113,7 +113,6 @@ admin.put('/modules', ...base, async (c) => {
113113
const [row] = await c.get('db')
114114
.select({
115115
modules: schema.settings.modules,
116-
selectionEnabled: schema.settings.selectionEnabled,
117116
aiChatEnabled: schema.settings.aiChatEnabled,
118117
aiVoiceEnabled: schema.settings.aiVoiceEnabled,
119118
})
@@ -124,7 +123,6 @@ admin.put('/modules', ...base, async (c) => {
124123
const modules = normalizeModulesConfig({ ...normalizeModulesConfig(row.modules, row), ...body });
125124
await updateSettings(c.get('db'), {
126125
modules,
127-
selectionEnabled: modules.ordering.enabled,
128126
aiChatEnabled: modules.ai.enabled,
129127
aiVoiceEnabled: modules.ai.enabled && modules.ai.voiceEnabled,
130128
});
@@ -147,10 +145,9 @@ admin.put('/settings', ...base, async (c) => {
147145
if (body.chatAgentPrompt !== undefined) updates.chatAgentPrompt = body.chatAgentPrompt;
148146
if (body.aiChatEnabled !== undefined) updates.aiChatEnabled = body.aiChatEnabled;
149147
if (body.aiVoiceEnabled !== undefined) updates.aiVoiceEnabled = body.aiChatEnabled === false ? false : body.aiVoiceEnabled;
150-
if (body.selectionEnabled !== undefined) updates.selectionEnabled = body.selectionEnabled;
151148
if (body.aiChatEnabled !== undefined || body.aiVoiceEnabled !== undefined || body.selectionEnabled !== undefined) {
152149
const [row] = await c.get('db')
153-
.select({ modules: schema.settings.modules, selectionEnabled: schema.settings.selectionEnabled, aiChatEnabled: schema.settings.aiChatEnabled, aiVoiceEnabled: schema.settings.aiVoiceEnabled })
150+
.select({ modules: schema.settings.modules, aiChatEnabled: schema.settings.aiChatEnabled, aiVoiceEnabled: schema.settings.aiVoiceEnabled })
154151
.from(schema.settings)
155152
.where(eq(schema.settings.id, 1));
156153
const modules = normalizeModulesConfig(row?.modules, row);
@@ -1076,7 +1073,7 @@ async function setLocaleFlag(
10761073

10771074
async function refreshPublicCatalog(c: Context<AppBindings>): Promise<void> {
10781075
try {
1079-
await warmCatalogAfterMutation(
1076+
await refreshCatalogArtifacts(
10801077
c.env,
10811078
c.req.url,
10821079
c.get('db'),

backend/src/routes/catalog.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
22
import { eq, asc } from 'drizzle-orm';
33
import { requireDb } from '../middleware/db';
44
import { requireAuth } from '../middleware/auth';
5-
import { attachDb, requireAdmin } from '../middleware/admin-guard';
5+
import { requireAdmin } from '../middleware/admin-guard';
66
import { RecordViewBodySchema, normalizeModulesConfig } from '@menu/schemas';
77
import type { CatalogResponse } from '@menu/schemas';
88
import * as schema from '../db/schema';
@@ -112,7 +112,7 @@ export const catalogRoutes = new Hono<AppBindings>()
112112
*
113113
* Admin-only. Regenerates the R2 snapshot from the DB.
114114
*/
115-
.post('/publish', requireAuth, attachDb, requireAdmin, async (c) => {
115+
.post('/publish', requireAuth, requireDb, requireAdmin, async (c) => {
116116
const bucket = c.env.PUBLIC_MENU_BUCKET;
117117
const db = c.get('db');
118118

@@ -279,15 +279,6 @@ export async function refreshCatalogArtifacts(
279279
};
280280
}
281281

282-
export async function warmCatalogAfterMutation(
283-
env: Env,
284-
requestUrl: string,
285-
db: DbInstance,
286-
publishedBy?: string,
287-
): Promise<void> {
288-
await refreshCatalogArtifacts(env, requestUrl, db, publishedBy);
289-
}
290-
291282
async function isMenuPublished(db: DbInstance): Promise<boolean> {
292283
const [row] = await db
293284
.select({ publicationState: schema.settings.publicationState })

packages/schemas/src/__tests__/common.test.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,7 @@
11
import { describe, it, expect } from 'vitest';
2-
import {
3-
MembershipRoleSchema,
4-
AllergenSchema,
5-
ExtrasTypeSchema,
6-
AnalyticsPeriodSchema,
7-
PublicationStateSchema,
8-
I18nMapSchema,
9-
} from '../common.js';
2+
import { I18nMapSchema } from '../common.js';
103

114
describe('common schemas', () => {
12-
it('MembershipRoleSchema accepts a known role, rejects others', () => {
13-
expect(MembershipRoleSchema.safeParse('manager').success).toBe(true);
14-
expect(MembershipRoleSchema.safeParse('intern').success).toBe(false);
15-
});
16-
it('AllergenSchema validates the enum', () => {
17-
expect(AllergenSchema.safeParse('Glutine').success).toBe(true);
18-
expect(AllergenSchema.safeParse('Peanuts').success).toBe(false);
19-
});
20-
it('ExtrasTypeSchema validates the enum', () => {
21-
expect(ExtrasTypeSchema.safeParse('zeroorone').success).toBe(true);
22-
expect(ExtrasTypeSchema.safeParse('many').success).toBe(false);
23-
});
24-
it('AnalyticsPeriodSchema validates the enum', () => {
25-
expect(AnalyticsPeriodSchema.safeParse('7d').success).toBe(true);
26-
expect(AnalyticsPeriodSchema.safeParse('1y').success).toBe(false);
27-
});
28-
it('PublicationStateSchema only allows draft|published', () => {
29-
expect(PublicationStateSchema.safeParse('published').success).toBe(true);
30-
expect(PublicationStateSchema.safeParse('archived').success).toBe(false);
31-
});
325
it('I18nMapSchema accepts locale->field->string, rejects non-string values', () => {
336
expect(I18nMapSchema.safeParse({ en: { name: 'Pizza' } }).success).toBe(true);
347
expect(I18nMapSchema.safeParse({ en: { name: 5 } }).success).toBe(false);

0 commit comments

Comments
 (0)