diff --git a/app/page.tsx b/app/page.tsx
index 3c7303b..a8dd538 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,119 +1,12 @@
import type { Metadata } from "next";
-import { promises as fs } from "node:fs";
-import path from "node:path";
+import { redirect } from "next/navigation";
export const metadata: Metadata = {
- title: "AdConsole",
- description: "Amazon PPC teaching simulator",
+ title: "AdConsole - Amazon PPC Simulator",
+ description: "Amazon PPC teaching simulator - Migrating to React",
};
-async function getLegacyStats() {
- try {
- const filePath = path.join(process.cwd(), "public", "adconsole.html");
- const stat = await fs.stat(filePath);
- return {
- bytes: stat.size,
- kb: Math.round(stat.size / 1024),
- modified: stat.mtime.toLocaleString("sv-SE"),
- };
- } catch (e) {
- if (process.env.NODE_ENV !== "production") {
- console.warn("adconsole.html not found:", e);
- }
- return null;
- }
-}
-
export default async function Home() {
- const stats = await getLegacyStats();
-
- return (
-
-
-
-
-
-
-
-
- Next.js · App Router · TypeScript · Tailwind v4
-
- Run npm run dev to start
-
-
-
- );
+ // Redirect directly to the legacy tool
+ redirect("/adconsole.html");
}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 05e726d..f23fd1e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -12,6 +12,8 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
+ // Project-specific ignores:
+ ".ijfw/**",
]),
]);
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..1655fec 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,40 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ async headers() {
+ return [
+ {
+ source: "/(.*)",
+ headers: [
+ {
+ key: "Content-Security-Policy",
+ value: [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net",
+ "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com",
+ "font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com",
+ "img-src 'self' data: https:",
+ "connect-src 'self'",
+ "frame-ancestors 'none'",
+ "form-action 'self'",
+ ].join("; "),
+ },
+ {
+ key: "X-Content-Type-Options",
+ value: "nosniff",
+ },
+ {
+ key: "X-Frame-Options",
+ value: "DENY",
+ },
+ {
+ key: "Referrer-Policy",
+ value: "strict-origin-when-cross-origin",
+ },
+ ],
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/public/adconsole.html b/public/adconsole.html
index 4c2de51..132d5c5 100644
--- a/public/adconsole.html
+++ b/public/adconsole.html
@@ -29,6 +29,7 @@
--sh2:0 4px 14px rgba(15,17,17,.14);
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
html{-webkit-text-size-adjust:100%}
html,body{height:100%}
body{font-family:'PT Sans',system-ui,sans-serif;background:var(--bg);color:var(--ink);font-size:14px;overflow:hidden}
@@ -45,12 +46,13 @@
.tb-left{display:flex;align-items:center;gap:12px;min-width:0}
.tb-icon{width:38px;height:38px;border-radius:6px;display:grid;place-items:center;color:#D5D9D9;border:1px solid transparent;transition:.15s;position:relative;flex:none}
.tb-icon:hover{border-color:#485769;color:#fff;background:rgba(255,255,255,.05)}
+.tb-icon:focus-visible{outline:2px solid #FF9900;outline-offset:2px}
.logo{display:flex;align-items:center;gap:2px;padding:6px 8px;border-radius:6px;border:1px solid transparent;cursor:pointer;transition:.15s;text-decoration:none;flex:none}
.logo:hover{border-color:#485769}
-.logo-t{color:#fff;font-family:Archivo;font-weight:700;font-size:19px;letter-spacing:-.3px;line-height:1;white-space:nowrap}
+.logo-t{color:#fff;font-family:Archivo;font-weight:700;font-size:19px;letter-spacing:-.3px;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.logo-t b{color:var(--orange);font-weight:700}
.smile{display:block;margin-top:1px}
-.tb-tag{font-size:11px;color:#99A5B4;border-left:1px solid #3A4553;padding-left:12px;line-height:1.25;white-space:nowrap}
+.tb-tag{font-size:11px;color:#99A5B4;border-left:1px solid #3A4553;padding-left:12px;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tb-tag em{display:block;font-style:normal;color:#6E7B8B;font-size:10px;letter-spacing:.6px;text-transform:uppercase}
.tb-search{flex:1;max-width:560px;display:flex;height:38px;border-radius:6px;overflow:hidden;background:#fff;box-shadow:0 0 0 3px transparent;transition:.15s;min-width:0}
.tb-search:focus-within{box-shadow:0 0 0 3px rgba(255,153,0,.45)}
@@ -75,10 +77,11 @@
.nv-sec{font-family:Archivo;font-size:10.5px;font-weight:600;letter-spacing:1.1px;color:var(--faint);text-transform:uppercase;padding:14px 18px 6px}
.nv{display:flex;align-items:center;gap:11px;width:calc(100% - 14px);margin:1px 7px;padding:9.5px 11px;border-radius:6px;color:#3B4149;font-size:13.5px;font-weight:400;position:relative;transition:.13s;text-align:left;border-left:3px solid transparent}
.nv:hover{background:#F2F4F4;color:var(--ink)}
+.nv:focus-visible{outline:2px solid #FF9900;outline-offset:-2px}
.nv.on{background:#FEF3E7;border-left-color:var(--orange);font-weight:700;color:var(--ink)}
.nv svg{flex:none;color:#5B6570}
.nv.on svg{color:var(--orange-d)}
-.nv-l{flex:1;white-space:nowrap;overflow:hidden}
+.nv-l{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nv-badge{font-family:Archivo;font-size:10.5px;font-weight:600;background:#EEF1F1;color:var(--sub);border-radius:9px;padding:1.5px 7px}
.nv.on .nv-badge{background:#FCE3C2;color:#8A5300}
.nv-sim{margin:18px 14px 4px;padding:11px 12px;border-radius:8px;background:linear-gradient(160deg,var(--navy) 0%,var(--navy2) 100%);color:#fff}
@@ -118,6 +121,7 @@
/* ---------- BUTTONS ---------- */
.btn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 14px;border-radius:7px;border:1px solid var(--border);background:#fff;color:var(--ink);font-size:13px;font-weight:700;font-family:Archivo;transition:.15s;white-space:nowrap}
.btn:hover{background:#F7FAFA;box-shadow:0 1px 3px rgba(15,17,17,.12)}
+.btn:focus-visible{outline:2px solid #FF9900;outline-offset:2px}
.btn:active{transform:translateY(1px)}
.btn-p{background:linear-gradient(180deg,#FFAD33,var(--orange));border-color:#E47911;color:var(--navy);position:relative;overflow:hidden}
.btn-p:hover{background:linear-gradient(180deg,#FFB84D,var(--orange-h))}
@@ -130,8 +134,8 @@
/* ---------- FORMS ---------- */
.inp,.sel,.tarea{height:34px;border:1px solid #8A939B;border-radius:7px;padding:0 10px;background:#fff;outline:none;transition:.13s;width:100%;min-width:0}
+.inp:focus,.sel:focus,.tarea:focus,.rsel:focus{outline:2px solid #FF9900;outline-offset:1px;border-color:var(--orange);box-shadow:0 0 0 3px rgba(255,153,0,.28)}
.tarea{height:auto;padding:8px 10px;resize:vertical}
-.inp:focus,.sel:focus,.tarea:focus{border-color:var(--orange);box-shadow:0 0 0 3px rgba(255,153,0,.28)}
.sel{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23565959' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
label.fld{display:block;font-family:Archivo;font-size:12px;font-weight:600;color:#3B4149;margin:0 0 5px}
.fhint{font-size:11.5px;color:var(--faint);margin-top:4px}
@@ -207,7 +211,7 @@
.stbtn:hover{background:#EEF1F1}
.stbtn svg{color:#8A939B}
-.editable{cursor:pointer;white-space:nowrap}
+.editable{cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.editable .pen{opacity:0;transition:.13s;margin-left:5px;color:#8A939B;vertical-align:-1px}
.editable:hover{color:var(--link)}
.editable:hover .pen{opacity:1}
@@ -546,6 +550,11 @@
+
+
@@ -577,7 +586,7 @@
-
+
Advertising
Dashboard
Campaigns 0
@@ -1081,10 +1090,20 @@ All ${LESSONS.length} missions compl
function perfChart(id,campId){
const cv=document.getElementById(id);if(!cv)return;
const b=rangeBounds();const s=seriesFor(campId,b);
+ // Helper: create segment colorizer for day-over-day comparison
+ const segColor = (ctx, colorUp, colorDown) => {
+ const p0 = ctx.p0.parsed.y;
+ const p1 = ctx.p1.parsed.y;
+ if (p0 == null || p1 == null) return colorUp;
+ return p1 >= p0 ? colorUp : colorDown;
+ };
const ds=[
- {label:'Sales',data:s.sal,borderColor:'#067D62',backgroundColor:'rgba(6,125,98,.10)',yAxisID:'y',fill:true},
- {label:'Spend',data:s.sp,borderColor:'#E47911',backgroundColor:'rgba(228,121,17,.08)',yAxisID:'y'},
- {label:'ACOS %',data:s.acos,borderColor:'#007185',borderDash:[5,4],yAxisID:'y1',pointStyle:'line'},
+ {label:'Sales',data:s.sal,borderColor:'#067D62',backgroundColor:'rgba(6,125,98,.10)',yAxisID:'y',fill:true,
+ segment:{borderColor:ctx=>segColor(ctx,'#067D62','#B12704')}},
+ {label:'Spend',data:s.sp,borderColor:'#E47911',backgroundColor:'rgba(228,121,17,.08)',yAxisID:'y',
+ segment:{borderColor:ctx=>segColor(ctx,'#E47911','#B12704')}},
+ {label:'ACOS %',data:s.acos,borderColor:'#007185',borderDash:[5,4],yAxisID:'y1',pointStyle:'line',
+ segment:{borderColor:ctx=>segColor(ctx,'#007185','#B12704')}},
];
charts[id]=new Chart(cv,{type:'line',data:{labels:s.labels,datasets:ds.map(d=>({...d,borderWidth:2,pointRadius:0,pointHitRadius:8,tension:.35,spanGaps:true}))},
options:{responsive:true,maintainAspectRatio:false,animation:{duration:250},interaction:{mode:'index',intersect:false},