Skip to content

Commit fd2b2fe

Browse files
Adding in new debug functionality
1 parent fef51f8 commit fd2b2fe

2 files changed

Lines changed: 266 additions & 98 deletions

File tree

Lines changed: 261 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Q, A, el } from "./utils.mjs";
1+
import { A, el } from "./utils.mjs";
22
import { setStyle } from "./styling.mjs";
33
import { CG } from "./CG.mjs";
44
import { pageHandlers } from "./router.mjs";
@@ -8,24 +8,31 @@ import { logger } from "./logger.mjs";
88
// static imports
99
import { config } from "../data/config.mjs";
1010

11-
/**
12-
* Update all links on the page to use either the production or staging domain.
13-
* @param {boolean} useTestDomain - If true, update links to use the staging domain; otherwise, use the production domain.
14-
* @returns {void}
15-
* @example
16-
* // Update links to use the staging domain
17-
* updateLinks(true);
18-
*
19-
* // Update links to use the production domain
20-
* updateLinks(false);
21-
*/
11+
// ── DEBUG_FLAGS ─────────────────────────────────────────────────────────────
12+
const FLAGS_KEY = "DEBUG_FLAGS";
13+
const DEFAULT_FLAGS = {
14+
disabled: false,
15+
logging: false,
16+
expose: false,
17+
useTestDomain: false,
18+
};
19+
20+
function getFlags() {
21+
try {
22+
return { ...DEFAULT_FLAGS, ...JSON.parse(localStorage.getItem(FLAGS_KEY) || "{}") };
23+
} catch {
24+
return { ...DEFAULT_FLAGS };
25+
}
26+
}
27+
28+
function setFlag(key, value) {
29+
localStorage.setItem(FLAGS_KEY, JSON.stringify({ ...getFlags(), [key]: value }));
30+
}
31+
32+
// ── domain swapper ──────────────────────────────────────────────────────────
2233
function updateLinks(useTestDomain) {
2334
A(
24-
'a[href*="' +
25-
config.domains.prod.url +
26-
'"], a[href*="' +
27-
config.domains.stage.url +
28-
'"]'
35+
`a[href*="${config.domains.prod.url}"], a[href*="${config.domains.stage.url}"]`
2936
).forEach((link) => {
3037
const url = new URL(link.href);
3138
if (useTestDomain && url.hostname === config.domains.prod.url) {
@@ -37,106 +44,263 @@ function updateLinks(useTestDomain) {
3744
});
3845
}
3946

40-
/**
41-
* Adds a debug heading with environment information and a staging toggle.
42-
* @returns {void}
43-
*/
44-
export function debugHeading() {
45-
// adding a dropdown info circle
46-
const infoCircle = el(
47-
"div",
48-
{ class: "align-vertical info-circle-wrapper" },
49-
[el("div", { class: "info-circle", text: "I" })]
50-
);
51-
CG.dom.headerRight.insertBefore(infoCircle, CG.dom.headerRight.firstChild);
52-
53-
let dropdownOptions = [
54-
el("span", {
55-
text: "Handler: " + pageHandlers.find(({ test }) => test).handler.name,
56-
}),
57-
el("input", {
58-
type: "checkbox",
59-
id: "cg-baseurl-staging",
60-
checked: CG.env.isStaging ? true : false,
61-
}),
62-
63-
// Add course edit link
64-
CG.state.course.id
65-
? el("a", { href: CG.state.course.edit, text: "Edit Course" })
66-
: null,
47+
// ── global exposure ─────────────────────────────────────────────────────────
48+
const GLOBALS = { logger, animateCompletion, shoot, el, setStyle, CG };
6749

68-
// Add path edit link
69-
CG.state.course.path.id && CG.state.domain
70-
? el("a", { href: CG.state.course.path.edit, text: "Edit Path" })
71-
: null,
72-
]
73-
.filter(Boolean)
74-
.map((html) => el("li", {}, [html]));
75-
76-
const dropdownMenu = el(
77-
"ul",
78-
{ class: "info-circle-menu", hidden: true },
79-
dropdownOptions
80-
);
50+
function exposeGlobals() { Object.assign(window, GLOBALS); }
51+
function unexposeGlobals() { Object.keys(GLOBALS).forEach((k) => delete window[k]); }
8152

82-
CG.dom.headerRight.parentElement.insertBefore(
83-
dropdownMenu,
84-
CG.dom.headerRight.parentElement.firstChild
85-
);
53+
// ── floating debug FAB ──────────────────────────────────────────────────────
54+
function buildDebugFab() {
55+
const styleEl = document.createElement("style");
56+
styleEl.textContent = `
57+
#cg-debug-fab {
58+
position: fixed;
59+
bottom: 20px;
60+
right: 20px;
61+
z-index: 99999;
62+
display: flex;
63+
align-items: center;
64+
justify-content: center;
65+
width: 36px;
66+
height: 36px;
67+
padding: 0;
68+
border: none;
69+
border-radius: 50%;
70+
background: #6226fb;
71+
color: #ffffff;
72+
font-size: 14px;
73+
font-weight: 800;
74+
font-family: Gellix, system-ui, sans-serif;
75+
letter-spacing: 0;
76+
cursor: pointer;
77+
box-shadow: 0 2px 10px rgba(98, 38, 251, 0.40);
78+
transition: transform 0.12s ease, box-shadow 0.12s ease;
79+
}
80+
#cg-debug-fab:hover {
81+
transform: scale(1.1);
82+
box-shadow: 0 4px 16px rgba(98, 38, 251, 0.55);
83+
}
84+
#cg-debug-panel {
85+
position: fixed;
86+
bottom: 64px;
87+
right: 20px;
88+
z-index: 99998;
89+
width: 232px;
90+
background: #ffffff;
91+
border: 1px solid #e2e8f0;
92+
border-radius: 12px;
93+
overflow: hidden;
94+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.06);
95+
color: #1e293b;
96+
font-family: Gellix, system-ui, sans-serif;
97+
font-size: 13px;
98+
}
99+
#cg-debug-panel[hidden] { display: none; }
100+
.cg-dbg-hd {
101+
display: flex;
102+
align-items: center;
103+
justify-content: space-between;
104+
padding: 10px 12px 9px;
105+
border-bottom: 1px solid #f1f5f9;
106+
}
107+
.cg-dbg-title {
108+
font-size: 11px;
109+
font-weight: 700;
110+
letter-spacing: 0.06em;
111+
text-transform: uppercase;
112+
color: #64748b;
113+
}
114+
.cg-dbg-close {
115+
display: flex;
116+
align-items: center;
117+
justify-content: center;
118+
width: 20px;
119+
height: 20px;
120+
padding: 0;
121+
border: none;
122+
border-radius: 4px;
123+
background: none;
124+
color: #94a3b8;
125+
font-size: 16px;
126+
line-height: 1;
127+
cursor: pointer;
128+
}
129+
.cg-dbg-close:hover { background: #f1f5f9; color: #1e293b; }
130+
.cg-dbg-section {
131+
padding: 6px 0;
132+
border-bottom: 1px solid #f1f5f9;
133+
}
134+
.cg-dbg-section:last-child { border-bottom: none; }
135+
.cg-dbg-row {
136+
display: flex;
137+
align-items: center;
138+
gap: 8px;
139+
padding: 5px 12px;
140+
cursor: pointer;
141+
user-select: none;
142+
}
143+
.cg-dbg-row:hover { background: #f8fafc; }
144+
.cg-dbg-row input[type="checkbox"] {
145+
flex-shrink: 0;
146+
width: 13px;
147+
height: 13px;
148+
margin: 0;
149+
accent-color: #6226fb;
150+
cursor: pointer;
151+
}
152+
.cg-dbg-row-lbl { font-size: 12px; }
153+
.cg-dbg-note {
154+
padding: 1px 12px 5px 33px;
155+
font-size: 10px;
156+
color: #94a3b8;
157+
line-height: 1.4;
158+
}
159+
.cg-dbg-info {
160+
display: flex;
161+
flex-direction: column;
162+
gap: 3px;
163+
padding: 8px 12px 6px;
164+
}
165+
.cg-dbg-kv { font-size: 11px; color: #94a3b8; }
166+
.cg-dbg-kv strong { color: #475569; font-weight: 600; }
167+
.cg-dbg-links {
168+
display: flex;
169+
flex-direction: column;
170+
gap: 2px;
171+
padding: 4px 12px 8px;
172+
}
173+
.cg-dbg-links a { font-size: 12px; color: #6226fb; text-decoration: none; }
174+
.cg-dbg-links a:hover { text-decoration: underline; }
175+
`;
176+
document.head.appendChild(styleEl);
86177

87-
const trigger = Q(".info-circle-wrapper");
88-
const dropdown = Q(".info-circle-menu");
178+
const flags = getFlags();
179+
const handler = pageHandlers.find(({ test }) => test)?.handler.name ?? "unknown";
180+
const envLabel = CG.env.isStaging ? "staging" : CG.env.isAdmin ? "admin" : "prod";
89181

90-
trigger.addEventListener("click", () => {
91-
const x = trigger.getBoundingClientRect().x;
182+
function makeToggle(label, flagKey, note, onChange) {
183+
const id = `cg-dbg-${flagKey}`;
184+
const checkbox = el("input", { type: "checkbox", id });
185+
checkbox.checked = flags[flagKey];
186+
checkbox.addEventListener("change", () => {
187+
setFlag(flagKey, checkbox.checked);
188+
onChange?.(checkbox.checked);
189+
});
190+
const row = el("label", { className: "cg-dbg-row", for: id }, [
191+
checkbox,
192+
el("span", { className: "cg-dbg-row-lbl", textContent: label }),
193+
]);
194+
return note
195+
? [row, el("div", { className: "cg-dbg-note", textContent: note })]
196+
: [row];
197+
}
92198

93-
const dropdownWidth = 200;
94-
const alignmentFactor = 0.7;
199+
const editLinks = [
200+
CG.state.course?.id
201+
? el("a", { href: CG.state.course.edit, textContent: "Edit Course", target: "_blank" })
202+
: null,
203+
CG.state.course?.path?.id && CG.state.domain
204+
? el("a", { href: CG.state.course.path.edit, textContent: "Edit Path", target: "_blank" })
205+
: null,
206+
].filter(Boolean);
95207

96-
const left = x - dropdownWidth * alignmentFactor;
208+
const panel = el("div", { id: "cg-debug-panel" }, [
209+
el("div", { className: "cg-dbg-hd" }, [
210+
el("span", { className: "cg-dbg-title", textContent: "CG Debug" }),
211+
el("button", { className: "cg-dbg-close", textContent: "×", aria: { label: "Close" } }),
212+
]),
213+
el("div", { className: "cg-dbg-section" }, [
214+
...makeToggle("Disable debug", "disabled", "Takes effect on next page load"),
215+
...makeToggle("Enable logging", "logging", null, (on) => {
216+
if (on) console.info("[CG] Logging enabled — reload for full output");
217+
}),
218+
...makeToggle("Expose globals", "expose", "logger, el, CG, shoot, …", (on) =>
219+
on ? exposeGlobals() : unexposeGlobals()
220+
),
221+
...makeToggle("Use test domain", "useTestDomain", null, updateLinks),
222+
]),
223+
el("div", { className: "cg-dbg-section" }, [
224+
el("div", { className: "cg-dbg-info" }, [
225+
el("div", { className: "cg-dbg-kv" }, [
226+
el("strong", { textContent: "Handler: " }),
227+
document.createTextNode(handler),
228+
]),
229+
el("div", { className: "cg-dbg-kv" }, [
230+
el("strong", { textContent: "Env: " }),
231+
document.createTextNode(envLabel),
232+
]),
233+
]),
234+
...(editLinks.length ? [el("div", { className: "cg-dbg-links" }, editLinks)] : []),
235+
]),
236+
]);
237+
panel.hidden = true;
97238

98-
dropdown.style.left = `${left}px`;
239+
panel.querySelector(".cg-dbg-close").addEventListener("click", () => {
240+
panel.hidden = true;
241+
});
99242

100-
dropdown.hidden = !dropdown.hidden;
243+
const fab = el("button", {
244+
id: "cg-debug-fab",
245+
textContent: "D",
246+
aria: { label: "Open debug panel" },
247+
});
248+
fab.addEventListener("click", (e) => {
249+
e.stopPropagation();
250+
panel.hidden = !panel.hidden;
101251
});
102252

103-
const checkbox = Q("#cg-baseurl-staging");
253+
document.body.append(fab, panel);
104254

105-
// initial state update if needed
106-
updateLinks(checkbox.checked);
255+
// apply initial domain state
256+
updateLinks(flags.useTestDomain);
107257

108-
// toggle behavior
109-
checkbox.addEventListener("change", function () {
110-
updateLinks(this.checked);
258+
// close on outside click or Escape
259+
document.addEventListener(
260+
"click",
261+
(e) => {
262+
if (!panel.hidden && !panel.contains(e.target) && e.target !== fab) {
263+
panel.hidden = true;
264+
}
265+
},
266+
{ capture: true }
267+
);
268+
document.addEventListener("keydown", (e) => {
269+
if (e.key === "Escape" && !panel.hidden) panel.hidden = true;
111270
});
112271
}
113272

114-
/*
115-
* Sets up logging based on the environment. Logging is enabled by default for staging and admin users, but can be toggled via localStorage.
116-
* This function also logs the current environment and state for debugging purposes.
117-
* It checks the environment variables to determine if logging should be enabled and stores this preference in localStorage.
118-
* Finally, it logs the current environment and state using the logger instance.
273+
/**
274+
* Adds a debug heading with environment information and a staging toggle.
275+
* @deprecated Use setupDebug() — the floating FAB supersedes this.
276+
* @returns {void}
277+
*/
278+
export function debugHeading() {}
279+
280+
/**
281+
* Sets up debug tooling. Reads and initialises DEBUG_FLAGS in localStorage,
282+
* builds the floating debug FAB, and (unless disabled) wires up logging,
283+
* global exposure, and environment logging.
284+
* @returns {void}
119285
*/
120286
export function setupDebug() {
121-
// setup logging based on environment - enabled for staging and admin users by default, but can be toggled
122-
if ((CG.env.isStaging || CG.env.isAdmin) && !localStorage.getItem("cg-logger-enabled")) {
123-
localStorage.setItem("cg-logger-enabled", "true");
124-
} else if (!localStorage.getItem("cg-logger-enabled")) {
125-
localStorage.setItem("cg-logger-enabled", "false");
287+
// first visit: seed flags based on environment
288+
if (localStorage.getItem(FLAGS_KEY) === null) {
289+
const auto = CG.env.isStaging || CG.env.isAdmin;
290+
localStorage.setItem(
291+
FLAGS_KEY,
292+
JSON.stringify({ ...DEFAULT_FLAGS, logging: auto, expose: auto })
293+
);
126294
}
127295

128-
// log environment info + state
296+
buildDebugFab();
297+
298+
const flags = getFlags();
299+
if (flags.disabled) return;
300+
129301
logger.info("Environment", CG.env);
130302
logger.info("State", CG.state);
131303
logger.info("Page", CG.page);
132304

133-
if (CG.env.isStaging || CG.env.isAdmin) {
134-
// Expose logger and animateCompletion to the global scope for debugging and external triggers
135-
window.logger = logger;
136-
window.animateCompletion = animateCompletion;
137-
window.shoot = shoot;
138-
window.el = el;
139-
window.setStyle = setStyle;
140-
window.CG = CG; // Expose CG for easier debugging access to state and environment
141-
}
142-
}
305+
if (flags.expose) exposeGlobals();
306+
}

0 commit comments

Comments
 (0)