diff --git a/backlog/features.json b/backlog/features.json index 16a429a..9ce6acd 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -7780,6 +7780,54 @@ }, "phase": "storefront", "completed_at": "2026-08-24T14:13:16Z" + }, + { + "id": "ADMIN-UI-FIXES2", + "type": "fix", + "title": "Admin UI fixes batch 2: trend chart, logs, KPI cards, AI timeout, X/Pinterest, product expiry/weight", + "description": "Need change", + "priority": "high", + "risk": "low", + "status": "pending", + "created_at": "2026-08-24", + "gates": { + "reviewer": false, + "security": false, + "qa": false + }, + "phase": "admin" + }, + { + "id": "FRONTEND-UI-FIXES2", + "type": "fix", + "title": "Frontend/Storefront fixes 2: live search BASE URL, product expiry/weight, TPV receipt clock", + "description": "Need change", + "priority": "high", + "risk": "low", + "status": "pending", + "created_at": "2026-08-24", + "gates": { + "reviewer": false, + "security": false, + "qa": false + }, + "phase": "frontend+storefront" + }, + { + "id": "INVENTORY-OPT", + "type": "fix", + "title": "Inventory optimization: pagination, queries, filters for 10k+ products", + "description": "Need change", + "priority": "med", + "risk": "low", + "status": "pending", + "created_at": "2026-08-24", + "gates": { + "reviewer": false, + "security": false, + "qa": false + }, + "phase": "backend" } ] } diff --git a/project/apps/admin/src/app/(dashboard)/page.tsx b/project/apps/admin/src/app/(dashboard)/page.tsx index dfa0b6f..2505771 100644 --- a/project/apps/admin/src/app/(dashboard)/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; import { api } from '@/lib/api-client'; +import Link from 'next/link'; interface Stats { ordersToday: number; @@ -47,15 +48,17 @@ function KPICard({ sub, icon, trend, + href, }: { label: string; value: string; sub?: string; icon: string; trend?: 'up' | 'down' | 'neutral'; + href?: string; }) { - return ( -
+ const inner = ( +

{label}

@@ -66,6 +69,7 @@ function KPICard({
); + return href ? {inner} : inner; } function OrderStateBar({ state, count, total }: { state: string; count: number; total: number }) { @@ -177,12 +181,14 @@ export default function DashboardPage() {
0 ? 'Revisar inventario' : 'Todo OK'} icon={stats.outOfStockVariants > 0 ? '🔴' : '✅'} + href="/inventory" />
diff --git a/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx b/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx index d7a6ed3..63d26f8 100644 --- a/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx @@ -79,7 +79,7 @@ function TrendDashboard({ filters }: { filters: FilterState }) { const maxValue = Math.max(...points.map((p) => p.value), 1); - return ; + return ; } function ChannelDashboard({ filters }: { filters: FilterState }) { diff --git a/project/apps/admin/src/app/(dashboard)/settings/page.tsx b/project/apps/admin/src/app/(dashboard)/settings/page.tsx index 789a907..56e8127 100644 --- a/project/apps/admin/src/app/(dashboard)/settings/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/settings/page.tsx @@ -153,6 +153,8 @@ export default function SettingsPage() {
{field('facebookUrl', 'Facebook', { placeholder: 'https://facebook.com/...' })} {field('instagramUrl', 'Instagram', { placeholder: 'https://instagram.com/...' })} + {field('twitterUrl', 'X (Twitter)', { placeholder: 'https://x.com/...' })} + {field('pinterestUrl', 'Pinterest', { placeholder: 'https://pinterest.com/...' })}
)} diff --git a/project/apps/admin/src/components/ServerLogViewer.tsx b/project/apps/admin/src/components/ServerLogViewer.tsx index 1b246c8..a17e66d 100644 --- a/project/apps/admin/src/components/ServerLogViewer.tsx +++ b/project/apps/admin/src/components/ServerLogViewer.tsx @@ -195,6 +195,11 @@ export function ServerLogViewer({ backendUrl = 'http://192.168.18.93:3000' }: Se style={{ minHeight: 0 }} > + + + + + {logs.map((entry, i) => ( {/* Message */} -
+ {/* Extra fields for HTTP request logs */} {entry.method && entry.url && ( diff --git a/project/apps/admin/src/components/reporting/TrendChart.tsx b/project/apps/admin/src/components/reporting/TrendChart.tsx index fd591ac..df91799 100644 --- a/project/apps/admin/src/components/reporting/TrendChart.tsx +++ b/project/apps/admin/src/components/reporting/TrendChart.tsx @@ -13,7 +13,7 @@ interface TrendChartProps { height?: number; } -export function TrendChart({ data, maxValue, height = 200 }: TrendChartProps) { +export function TrendChart({ data, maxValue, height = 120 }: TrendChartProps) { if (data.length === 0) return null; const width = 100; // percentage-based SVG diff --git a/project/migrations/062_settings_twitter_pinterest.js b/project/migrations/062_settings_twitter_pinterest.js new file mode 100644 index 0000000..ace87c9 --- /dev/null +++ b/project/migrations/062_settings_twitter_pinterest.js @@ -0,0 +1,13 @@ +/** + * Add twitter_url and pinterest_url columns to store_settings table. + * Usage: node --env-file=.env -e "require('./migrations/062_settings_twitter_pinterest.js')" + */ +import { query } from '../src/infrastructure/db/migrate.js'; + +async function migrate() { + await query(`ALTER TABLE store_settings ADD COLUMN IF NOT EXISTS twitter_url TEXT`); + await query(`ALTER TABLE store_settings ADD COLUMN IF NOT EXISTS pinterest_url TEXT`); + console.log('✓ Added twitter_url and pinterest_url to store_settings'); +} + +migrate().catch(e => { console.error(e); process.exit(1); }); diff --git a/project/src/modules/store-settings/api/settings.routes.ts b/project/src/modules/store-settings/api/settings.routes.ts index 847c8ce..9e39abe 100644 --- a/project/src/modules/store-settings/api/settings.routes.ts +++ b/project/src/modules/store-settings/api/settings.routes.ts @@ -21,6 +21,8 @@ const updateSettingsSchema = z.object({ footerText: z.string().max(400).optional(), facebookUrl: z.string().url().optional().or(z.literal('')), instagramUrl: z.string().url().optional().or(z.literal('')), + twitterUrl: z.string().url().optional().or(z.literal('')), + pinterestUrl: z.string().url().optional().or(z.literal('')), aiProvider: z.string().max(80).optional(), aiBaseUrl: z.string().url().optional().or(z.literal('')), aiModel: z.string().max(120).optional(), @@ -69,6 +71,8 @@ const SETTING_KEYS: Record = { footerText: 'footer_text', facebookUrl: 'facebook_url', instagramUrl: 'instagram_url', + twitterUrl: 'twitter_url', + pinterestUrl: 'pinterest_url', aiProvider: 'ai_provider', aiBaseUrl: 'ai_base_url', aiModel: 'ai_model', @@ -117,6 +121,8 @@ export async function registerStoreSettingsRoutes( footerText: map['footer_text'] ?? '', facebookUrl: map['facebook_url'] ?? '', instagramUrl: map['instagram_url'] ?? '', + twitterUrl: map['twitter_url'] ?? '', + pinterestUrl: map['pinterest_url'] ?? '', aiProvider: map['ai_provider'] ?? '', aiBaseUrl: map['ai_base_url'] ?? '', aiModel: map['ai_model'] ?? '', @@ -206,6 +212,8 @@ export async function registerStoreSettingsRoutes( footerText: map['footer_text'] ?? '', facebookUrl: map['facebook_url'] ?? '', instagramUrl: map['instagram_url'] ?? '', + twitterUrl: map['twitter_url'] ?? '', + pinterestUrl: map['pinterest_url'] ?? '', aiProvider: map['ai_provider'] ?? '', aiBaseUrl: map['ai_base_url'] ?? '', aiModel: map['ai_model'] ?? '', diff --git a/project/src/modules/store-settings/domain/settings.ts b/project/src/modules/store-settings/domain/settings.ts index e9831b5..eef83c0 100644 --- a/project/src/modules/store-settings/domain/settings.ts +++ b/project/src/modules/store-settings/domain/settings.ts @@ -14,4 +14,6 @@ export interface StoreSettings { footerText: string; facebookUrl: string; instagramUrl: string; + twitterUrl: string; + pinterestUrl: string; } diff --git a/project/src/shared/ai-content.ts b/project/src/shared/ai-content.ts index 1a6a891..f11c204 100644 --- a/project/src/shared/ai-content.ts +++ b/project/src/shared/ai-content.ts @@ -1,5 +1,8 @@ import { AppError } from './errors.js'; +/** Default AI timeout: 2 minutes for complex product descriptions */ +const AI_TIMEOUT_MS = Number.parseInt(process.env.AI_TIMEOUT_MS ?? '120000', 10); + /** * Shared helpers for AI content generation (OpenAI-compatible chat API) * and HTML formatting of the generated text. @@ -15,7 +18,7 @@ export async function generateWithModel( method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }], temperature: 0.4 }), - signal: AbortSignal.timeout(30_000), + signal: AbortSignal.timeout(AI_TIMEOUT_MS), }); const payload = (await response.json().catch(() => null)) as { choices?: Array<{ message?: { content?: unknown } }>; diff --git a/work/runtime-status.json b/work/runtime-status.json index 2cd4983..8ae59ee 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,11 +1,19 @@ { - "feature_id": "FRONTEND-FIXES", + "feature_id": "ADMIN-UI-FIXES2", "stage": "build", "agent": "implementer", "action": "", "state": "running", "next_agent": "leader", "waiting_for": "Seleccionar una feature pending y actualizar este estado", - "updated_at": "2026-08-24T14:15:00Z", - "timeline": [] + "updated_at": "2026-08-24T20:06:08Z", + "timeline": [ + { + "ts": "2026-08-24T20:06:08Z", + "agent": "implementer", + "stage": "build", + "state": "running", + "message": "Estado actualizado" + } + ] }