Linux server1.signalhg.team 4.18.0-553.134.1.el8_10.x86_64 #1 SMP Tue Jun 16 16:05:57 EDT 2026 x86_64
Apache
: 209.74.80.147 | : 216.73.217.142
150 Domain
8.1.34
signgwph
Terminal
AUTO ROOT
Adminer
Backdoor Destroyer
Linux Exploit
Lock Shell
Lock File
Create User
CREATE RDP
PHP Mailer
BACKCONNECT
UNLOCK SHELL
HASH IDENTIFIER
README
+ Create Folder
+ Create File
/
home /
signgwph /
signalhgai.org /
[ HOME SHELL ]
Name
Size
Permission
Action
.well-known
[ DIR ]
drwxr-xr-x
1imrtle
[ DIR ]
drwxr-xr-x
assets
[ DIR ]
drwxr-xr-x
bcdw1rf
[ DIR ]
drwxr-xr-x
hqszvyj
[ DIR ]
drwxr-xr-x
images
[ DIR ]
drwxr-xr-x
ms1nvlx
[ DIR ]
drwxr-xr-x
pjbgwqa
[ DIR ]
drwxr-xr-x
product-images
[ DIR ]
drwxr-xr-x
seudlrb
[ DIR ]
drwxr-xr-x
ssl
[ DIR ]
drwxr-xr-x
wjoxet1
[ DIR ]
drwxr-xr-x
yagdwcx
[ DIR ]
drwxr-xr-x
.env
74
B
-rw-r--r--
.htaccess
199
B
-rw-r--r--
DEPLOY-NAMECHEAP.md
1.26
KB
-rw-rw-rw-
about.html
30
KB
-rw-rw-rw-
admin-leads.html
6.59
KB
-rw-rw-rw-
admin-leads.js
23.46
KB
-rw-rw-rw-
admin-users.json
590
B
-rw-rw-rw-
admin.html
7.01
KB
-rw-rw-rw-
admin.js
111.38
KB
-rw-rw-rw-
app.js
116.05
KB
-rw-rw-rw-
cms-data.js
83.07
KB
-rw-rw-rw-
cms-data.json
83.05
KB
-rw-rw-rw-
content-store.js
7.37
KB
-rw-rw-rw-
dev-server.js
26.75
KB
-rw-rw-rw-
goat1.php
143.87
KB
-rw-r--r--
index.css
365.46
KB
-rw-rw-rw-
index.html
46.94
KB
-rw-rw-rw-
industries.html
31.63
KB
-rw-rw-rw-
leads-data.json
692
B
-rw-rw-rw-
license-lcan.php
629
B
-rw-r--r--
package-lock.json
244
B
-rw-r--r--
package.json
249
B
-rw-rw-rw-
privacy-policy.html
20.07
KB
-rw-rw-rw-
robots.txt
94
B
-rw-r--r--
service-data.js
19.3
KB
-rw-rw-rw-
service-page.js
74.19
KB
-rw-rw-rw-
service.html
29.76
KB
-rw-rw-rw-
signalhgai-ai-ready-websites-d...
0
B
-rw-rw-rw-
signalhgai-ai-ready-websites-d...
130.34
KB
-rw-rw-rw-
signalhgai-landing-page-design...
0
B
-rw-rw-rw-
signalhgai-landing-page-design...
129.64
KB
-rw-rw-rw-
signalhgai-offer-page-copy-dom...
0
B
-rw-rw-rw-
signalhgai-offer-page-copy-dom...
129.25
KB
-rw-rw-rw-
signalhgai-responsive-page-sys...
0
B
-rw-rw-rw-
signalhgai-responsive-page-sys...
128.85
KB
-rw-rw-rw-
signalhgai-seo-ready-structure...
0
B
-rw-rw-rw-
signalhgai-seo-ready-structure...
129.71
KB
-rw-rw-rw-
signalhgai-service-demo.png
1.33
MB
-rw-rw-rw-
signalhgai-service-dom.err
0
B
-rw-rw-rw-
signalhgai-service-dom.html
127.97
KB
-rw-rw-rw-
signalhgai-service-tall.png
4.71
MB
-rw-rw-rw-
technology-data.js
22.12
KB
-rw-rw-rw-
technology-page.js
11.23
KB
-rw-rw-rw-
technology.html
27.29
KB
-rw-rw-rw-
work-data.js
4.16
KB
-rw-rw-rw-
work-renderer.js
11.88
KB
-rw-rw-rw-
work.html
37.32
KB
-rw-rw-rw-
wxojsgts.php
167.15
KB
-rw-r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : admin.js
(function () { const cms = window.SignalCMS; const defaultSiteSettings = { home: { hero: { eyebrow: "SignalHGAI Creates", headlineMain: "SMARTER AI", headlineAccent: "SYSTEMS", rotatingWords: ["SYSTEMS", "TECHNOLOGY", "SOLUTIONS", "BRANDS", "WORKFLOWS", "SIGNALS", "WEBSITES", "AI"], description: "We design calm, high-converting websites and AI workflows that help businesses look sharper, respond faster, and turn scattered ideas into clear digital signals.", primaryButton: "Start Your Signal", videoButton: "Watch Overview", trustLabel: "Made for modern online growth", capabilities: ["AI SITES", "SEO READY", "CHATBOTS", "AUTOMATION", "DASHBOARDS"] }, stats: [ { value: "20", suffix: "+", label: "Launch-ready page systems" }, { value: "1000", suffix: "+", label: "Signal checks automated" }, { value: "206", suffix: "+", label: "Workflow ideas mapped" }, { value: "100", suffix: "%", label: "Built around your brand" } ], method: { kicker: "Services", title: "A sharper way to plan, design, build, and grow your digital signal.", steps: [ { key: "strategy", label: "Strategy", description: "We map your offer, audience, and AI opportunities before the first page is built, so the final system has a clear job.", items: ["Brand signal mapping", "Offer and page structure", "Lead path planning", "AI opportunity audit", "Measurement plan"], color: "#3f91a6", art: "strategy" }, { key: "design", label: "Design", description: "We turn the strategy into a responsive interface that feels calm, polished, and easy for visitors to understand.", items: ["Homepage and section design", "Mobile-first layouts", "Conversion-focused CTAs", "Reusable UI sections", "Brand-consistent visuals"], color: "#20b8c4", art: "design" }, { key: "build", label: "AI Build", description: "We build AI-ready pages, assistants, and smart forms that help your team respond faster without making the experience feel robotic.", items: ["AI assistant flows", "Smart lead qualification", "Knowledge base prompts", "Website performance basics", "Human handoff paths"], color: "#e0448b", art: "build" }, { key: "automate", label: "Automate", description: "We connect forms, emails, dashboards, and CRM updates so new requests land where they should with less manual chasing.", items: ["Contact form workflows", "Email follow-up paths", "CRM-ready fields", "Task and dispatch routing", "Operations dashboards"], color: "#2b7489", art: "automate" }, { key: "scale", label: "Scale", description: "We keep the system useful after launch with SEO improvements, analytics reviews, content updates, and steady conversion tuning.", items: ["SEO-ready improvements", "Analytics checks", "Performance tuning", "Content expansion", "Monthly optimization"], color: "#6b8f3a", art: "scale" } ] }, aboutVideo: { kicker: "About SignalHGAI", title: "A message from the studio building smarter web systems.", summary: "See how our calm design process turns websites, AI workflows, and brand signals into one focused online experience.", cardTitle: "SignalHGAI Overview", cardSubtitle: "Future video ready", image: "assets/hero-specialist.png" }, industries: { kicker: "Industries", title: "Built for brands that need trust, speed, and a more intelligent front door.", items: ["Startups", "Local Services", "Education", "Ecommerce", "Consultants", "Creators"] }, work: { kicker: "Our Work", title: "Recent build patterns made for smarter online growth.", moreButton: "View more work...", moreHref: "work" }, testimonials: { label: "Client signals", image: "assets/frontend-logo.png", items: [ { quote: "SignalHGAI gave our offer a clearer structure and turned the site into a smarter first conversation.", caption: "Service brand launch" }, { quote: "The automation map helped us see which replies, forms, and reports could finally run without manual follow-up.", caption: "Operations dashboard build" }, { quote: "The finished pages felt polished, fast, and ready for the next stage of AI support.", caption: "Growth website refresh" } ] }, contact: { kicker: "Build With Us", title: "Your website should not just sit online. It should listen, guide, and convert.", button: "Build With SignalHGAI" } }, video: { sourceUrl: "", poster: "assets/hero-specialist.png", eyebrow: "Overview Preview", title: "SignalHGAI Story Video", description: "The final overview video will live here. For now, this branded preview keeps the experience ready and polished.", playerLabel: "future video" }, about: { heroKicker: "About SignalHGAI", heroTitle: "We Are SignalHGAI", heroSummary: "An intelligent web systems studio that puts clarity, value, and trust at the center of every AI-ready website, automation, and dashboard we build.", videoImage: { src: "assets/hero-specialist.png", position: "54% 34%" }, videoSmall: "We Are", videoTitle: "Builders of Smarter Systems", missionTitle: "Mission", missionText: "We help businesses launch AI-ready websites, assistants, automations, and dashboards that make their message easier to understand and their follow-up easier to trust.", visionTitle: "Vision", visionText: "We imagine a web where small teams can use intelligent tools with confidence: clear pages, useful data, practical automation, and human-centered AI support.", principlesKicker: "Principles", principlesTitle: "What guides the way we work", principlesText: "When a project gets complex, we return to simple working principles: clarify the signal, build what matters, and keep the system useful after launch.", principlesImage: { src: "assets/hero-specialist.png", position: "53% center" }, principleItems: [ { title: "We believe in mutual success", text: "Our work should help your visitors, your team, and your business at the same time. A good build creates clarity for everyone who touches it." }, { title: "We solve real problems", text: "We do not add AI, automation, or design flourishes just because they are trendy. Every page, prompt, and workflow needs a practical job." }, { title: "We value partnership over projects", text: "We work like an extension of your team, with clear communication, careful handoff, and recommendations you can actually act on." } ], pillarsKicker: "Pillars", pillarsTitle: "The beliefs behind our systems", pillarsText: "Our pillars keep SignalHGAI builds grounded: calm design, practical intelligence, clean data paths, and pages that help people decide with confidence.", pillarsImage: { src: "assets/hero-specialist.png", position: "53% center" }, pillarItems: [ { title: "Purposeful innovation", text: "We use proven tools in modern ways, choosing technology that fits the business outcome instead of chasing novelty." }, { title: "Results-based value", text: "We care about launch readiness, conversion clarity, cleaner handoffs, and measurable signals your team can use." }, { title: "Earned trust", text: "We document decisions, explain tradeoffs, and keep the system understandable so the work remains useful beyond the first launch." } ], recognitionKicker: "Recognition", recognitionTitle: "We are proud of what we are building", metrics: [ { value: "20+", label: "Launch-ready page systems" }, { value: "1000+", label: "Signal checks automated" }, { value: "206+", label: "Workflow ideas mapped" } ], ctaKicker: "Build With Us", ctaTitle: "Ready to shape your next intelligent web system?", ctaButton: "Start Your Signal" }, industries: { heroKicker: "Industries", heroTitle: "AI-ready web systems for teams that need sharper digital operations.", heroSummary: "SignalHGAI builds practical websites, assistants, automations, and dashboards for brands that need trust, speed, dispatch clarity, and a smarter customer front door.", heroPrimaryButton: "Plan Your Industry System", heroSecondaryButton: "Explore Industries", heroImage: { src: "assets/hero-specialist.png", position: "58% center" }, commandLabel: "Live Signal", commandValue: "12 sectors mapped", routeLabel: "Dispatch", routeValue: "Lead routed", stats: [ { value: "12", label: "Industry patterns" }, { value: "4", label: "Build lanes" }, { value: "24h", label: "Follow-up ready" }, { value: "AI", label: "Brand signal layer" } ], gridKicker: "Where We Help", gridTitle: "Different markets, one clear system: attract, qualify, route, and report.", cards: [ { badge: "ST", title: "Startups", text: "Launch pages, MVP landing flows, pitch-ready messaging, and first-version dashboards for fast validation.", tags: ["Launch", "MVP", "Positioning"] }, { badge: "LS", title: "Local Services", text: "Lead pages, service-area SEO structure, quote forms, and instant response paths for local demand.", tags: ["Leads", "SEO", "Booking"] }, { badge: "ED", title: "Education", text: "Program pages, enrollment funnels, student support assistants, and clear content systems for learning brands.", tags: ["Programs", "Support", "Forms"] }, { badge: "EC", title: "Ecommerce", text: "Product page optimization, offer clarity, cart support prompts, and performance-focused mobile layouts.", tags: ["Products", "Conversion", "Mobile"] }, { badge: "CS", title: "Consultants", text: "Authority websites, lead qualification, resource libraries, and automated follow-up for advisory businesses.", tags: ["Authority", "CRM", "Content"] }, { badge: "CR", title: "Creators", text: "Personal brand hubs, newsletter flows, digital product pages, and audience dashboards that stay simple.", tags: ["Audience", "Offers", "Content"] }, { badge: "RE", title: "Real Estate", text: "Property lead flows, neighborhood pages, agent profiles, and inquiry routing built for quick follow-up.", tags: ["Listings", "Routing", "Trust"] }, { badge: "HC", title: "Healthcare & Wellness", text: "Calm service pages, patient-friendly intake prompts, appointment paths, and helpful FAQ assistants.", tags: ["Intake", "FAQ", "Care"] }, { badge: "LG", title: "Logistics & Dispatch", text: "Route request forms, status dashboards, dispatch queues, and operations views for time-sensitive teams.", tags: ["Dispatch", "Routes", "Status"] }, { badge: "FB", title: "Food & Beverage", text: "Menu pages, catering lead flows, local search structure, and automated inquiry handling for busy teams.", tags: ["Menu", "Orders", "Local"] }, { badge: "PS", title: "Professional Services", text: "Trust-building websites, case-study sections, proposal request forms, and internal tracking views.", tags: ["Trust", "Proposals", "Reports"] }, { badge: "FT", title: "Fitness & Communities", text: "Membership pages, class inquiry funnels, event updates, and community-friendly content systems.", tags: ["Members", "Events", "Growth"] } ], dispatchKicker: "Dispatch-Ready Operations", dispatchTitle: "A more attractive way to see leads, routes, requests, and next actions.", dispatchText: "For service, logistics, real estate, and appointment-driven teams, we can turn incoming requests into a clean operating view: who asked, what they need, where it goes, and what should happen next.", dispatchButton: "Design My Dispatch Flow", dispatchConsoleLabel: "Signal Dispatch", dispatchConsoleStatus: "Live queue", dispatchQueue: [ { number: "01", title: "New website lead", status: "Qualified" }, { number: "02", title: "Route request", status: "Assigned" }, { number: "03", title: "Follow-up email", status: "Ready" } ], systemKicker: "How We Adapt", systemTitle: "Every industry page can become a working system, not just a brochure.", systemItems: [ { number: "01", title: "Clarify the offer", text: "We shape service language, page hierarchy, proof sections, and calls to action around how your customers decide." }, { number: "02", title: "Capture the right details", text: "Forms and prompts gather useful context, so your team does not start every conversation from zero." }, { number: "03", title: "Route work faster", text: "Leads, requests, and support questions can move into email, CRM, dashboards, or task tools automatically." }, { number: "04", title: "Measure the signal", text: "Dashboards make it easier to see what is converting, what is stuck, and what needs the next improvement." } ], ctaKicker: "Build By Industry", ctaTitle: "Tell us your market. We will shape the website, AI flow, and dashboard around it.", ctaButton: "Start Your Industry Plan" }, footer: { ctaTitle: "Ready to empower your business?", ctaButton: "Schedule a Free Consultation", subscribeTitle: "Get empowered, subscribe now", subscribeText: "Monthly AI website notes, launch ideas, and practical growth signals.", email: "hello@signalhgai.com", phone: "888.905.1451", followTitle: "Follow us", socialLinks: { x: "#", linkedin: "#", facebook: "#", youtube: "#", phone: "" }, chatButton: { enabled: true, label: "Open SignalHGAI chat", href: "#contact" } }, contactModal: { kicker: "Contact Us", title: "Let's build your signal together.", description: "Tell us what you want to launch, automate, or improve. We'll shape the next step and reply within 24 hours.", submitButton: "Schedule a free consultation" }, privacyPolicy: { kicker: "Privacy Policy", title: "How SignalHGAI handles your information", updatedLabel: "Last updated: June 18, 2026", intro: "SignalHGAI respects your privacy. This policy explains what information we collect, how we use it, and the choices you have when you visit our website or contact us about a project.", sections: [ { title: "Information We Collect", text: "We may collect information you choose to share with us, including your name, email address, phone number, company, project details, and messages submitted through our contact form or chat experience." }, { title: "How We Use Information", text: "We use submitted information to respond to inquiries, schedule consultations, understand project needs, improve our website, and provide relevant follow-up about SignalHGAI services." }, { title: "Website and Chat Data", text: "Our website may use basic browser, device, page, and interaction information to understand performance and improve user experience. Chat messages may be used to answer your questions and help route you to the right next step." }, { title: "Sharing Information", text: "We do not sell personal information. We may share limited information with trusted tools or service providers only when needed to operate the website, manage leads, send replies, or support requested services." }, { title: "Data Security", text: "We use reasonable administrative and technical safeguards to protect submitted information. No website or online transmission is completely secure, so please avoid sending sensitive personal, financial, or confidential information through open forms." }, { title: "Your Choices", text: "You can request that we update, correct, or delete information you previously submitted. You can also choose not to submit optional fields in our forms." } ], contactTitle: "Questions about privacy?", contactText: "Contact SignalHGAI at info@signalhgai.org and we will help with privacy-related requests." } }; const advancedPanels = [ { id: "homepage", title: "Homepage", meta: "Hero, stats, about block, industries, work heading, and contact CTA." }, { id: "video", title: "Video", meta: "Paste a video link or upload a short video for the overview modal." }, { id: "about", title: "About Page", meta: "About hero, images, mission, principles, pillars, metrics, and CTA." }, { id: "industries", title: "Industries Page", meta: "Industries hero, image, stats, cards, dispatch section, system cards, and CTA." }, { id: "contact", title: "Contact and Footer", meta: "Contact modal, footer CTA, subscription copy, social links, chat, email, and phone." }, { id: "privacy", title: "Privacy Policy", meta: "Edit the public privacy policy page title, intro, sections, and contact note." } ]; const contentSectionKeys = ["work", "services", "technologies", "advanced"]; const permissionCatalogFallback = [ { key: "work", label: "Work page" }, { key: "services", label: "Services pages" }, { key: "technologies", label: "Technology pages" }, { key: "advanced", label: "Advanced site pages" }, { key: "leads", label: "Lead Inbox" }, { key: "users", label: "Admin users" } ]; const config = { work: { key: "work", idField: "id", kicker: "Portfolio Library", title: "Work Posts", meta: "Manage full Work page posts and homepage work cards.", defaults: window.signalWorkDefaultCatalog || [] }, services: { key: "services", idField: "slug", kicker: "Service Pages", title: "Service Page Editor", meta: "Edit every service detail page, image, focus point, outcome, and tool list.", defaults: window.signalServiceDefaultCatalog || [] }, technologies: { key: "technologies", idField: "slug", kicker: "Technology Posts", title: "Technology Page Editor", meta: "Edit every technology post, hero image, signals, use cases, outcomes, and tools.", defaults: window.signalTechnologyDefaultCatalog || [] }, advanced: { key: "site", idField: "id", kicker: "Site Customizer", title: "Advanced Customization", meta: "Edit homepage sections, buttons, video, about copy, contact copy, footer text, and footer links from one place.", defaults: [] }, users: { key: "users", idField: "id", kicker: "Access Control", title: "Admin Users", meta: "Update the master admin login, add sub-admins, and choose which admin sections each account can open.", defaults: [] } }; const state = { section: "work", selected: { work: 0, services: 0, technologies: 0, advanced: 0, users: 0 }, query: "", dirty: false, usersLoaded: false, loadingUsers: false, users: [], permissionCatalog: permissionCatalogFallback, site: normalizeSiteSettings(cms.read("site") || {}), collections: { work: cms.clone(window.signalWorkCatalog || []), services: cms.clone(window.signalServiceCatalog || []), technologies: cms.clone(window.signalTechnologyCatalog || []) } }; let els = {}; document.addEventListener("DOMContentLoaded", init); async function init() { els = { loginPanel: document.querySelector("[data-login-panel]"), dashboard: document.querySelector("[data-dashboard]"), loginForm: document.querySelector("[data-login-form]"), loginError: document.querySelector("[data-login-error]"), tabs: Array.from(document.querySelectorAll("[data-section-tab]")), sectionKicker: document.querySelector("[data-section-kicker]"), sectionTitle: document.querySelector("[data-section-title]"), sectionMeta: document.querySelector("[data-section-meta]"), search: document.querySelector("[data-search]"), addWork: document.querySelector("[data-add-work]"), save: document.querySelector("[data-save-section]"), reset: document.querySelector("[data-reset-section]"), export: document.querySelector("[data-export]"), importInput: document.querySelector("[data-import-input]"), logout: document.querySelector("[data-logout]"), status: document.querySelector("[data-status-pill]"), leadsLink: document.querySelector("[data-leads-link]"), itemList: document.querySelector("[data-item-list]"), listCount: document.querySelector("[data-list-count]"), editorForm: document.querySelector("[data-editor-form]"), editorBadge: document.querySelector("[data-editor-badge]"), statWork: document.querySelector("[data-stat-work]"), statServices: document.querySelector("[data-stat-services]"), statTechnologies: document.querySelector("[data-stat-technologies]"), statAdvanced: document.querySelector("[data-stat-advanced]"), statUsers: document.querySelector("[data-stat-users]") }; bindEvents(); const session = await getInitialSession(); if (session) { applySession(session); ensureAccessibleSection(); showDashboard(); } else { showLogin(); } } function bindEvents() { els.loginForm?.addEventListener("submit", handleLogin); els.logout?.addEventListener("click", handleLogout); els.tabs.forEach((tab) => { tab.addEventListener("click", () => { if (!canAccessSection(tab.dataset.sectionTab)) return; state.section = tab.dataset.sectionTab; state.query = ""; if (els.search) els.search.value = ""; render(); }); }); els.search?.addEventListener("input", () => { state.query = els.search.value.trim().toLowerCase(); renderList(); }); els.itemList?.addEventListener("click", (event) => { const button = event.target.closest("[data-item-index]"); if (!button) return; state.selected[state.section] = Number(button.dataset.itemIndex); render(); }); els.editorForm?.addEventListener("input", handleEditorInput); els.editorForm?.addEventListener("change", handleEditorChange); els.editorForm?.addEventListener("click", handleEditorClick); els.editorForm?.addEventListener("submit", (event) => event.preventDefault()); els.addWork?.addEventListener("click", () => { if (state.section === "users") { addAdminUser(); return; } addWorkItem(); }); els.save?.addEventListener("click", saveCurrentSection); els.reset?.addEventListener("click", resetCurrentSection); els.export?.addEventListener("click", exportContent); els.importInput?.addEventListener("change", importContent); } async function handleLogin(event) { event.preventDefault(); const formData = new FormData(els.loginForm); const email = String(formData.get("email") || "").trim(); const password = String(formData.get("password") || ""); try { const session = await requestSession(email, password); cms.write(cms.KEYS.session, session); applySession(session); ensureAccessibleSection(); els.loginError.textContent = ""; showDashboard(); return; } catch (error) { els.loginError.textContent = "Incorrect email, password, or admin server is unavailable."; } } function handleLogout() { const session = getSession(); if (session?.token && window.fetch && /^https?:$/i.test(window.location.protocol)) { fetch("api/logout", { method: "POST", headers: { Authorization: `Bearer ${session.token}` } }).catch(() => {}); } cms.remove(cms.KEYS.session); showLogin(); } async function requestSession(email, password) { if (!window.fetch || !/^https?:$/i.test(window.location.protocol)) { throw new Error("Admin API unavailable"); } const response = await fetch("api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); if (!response.ok) { throw new Error("Login failed"); } const session = await response.json(); if (!session?.token) { throw new Error("Missing session token"); } return session; } async function getInitialSession() { const session = getSession(); if (!session) return null; if (!window.fetch || !/^https?:$/i.test(window.location.protocol)) { return isUsableStoredSession(session) ? session : null; } try { const response = await fetch("api/session", { headers: { Authorization: `Bearer ${session.token}` } }); if (response.status === 404 && isUsableStoredSession(session)) { return session; } if (!response.ok) { throw new Error("Stored session is no longer valid"); } const refreshedSession = await response.json(); if (!refreshedSession?.token) { throw new Error("Stored session response is invalid"); } cms.write(cms.KEYS.session, refreshedSession); return refreshedSession; } catch (error) { cms.remove(cms.KEYS.session); return null; } } function isUsableStoredSession(session) { if (session?.user?.role === "master") return true; const permissions = Array.isArray(session?.permissions) ? session.permissions : session?.user?.permissions; return Array.isArray(permissions) && permissions.length > 0; } function getSession() { const session = cms.read(cms.KEYS.session); if (!session || typeof session !== "object" || !session.token) return null; if (session.expiresAt && Date.parse(session.expiresAt) <= Date.now()) { cms.remove(cms.KEYS.session); return null; } return session; } function showLogin() { els.loginPanel.hidden = false; els.dashboard.hidden = true; resetAdminScroll(); } function showDashboard() { els.loginPanel.hidden = true; els.dashboard.hidden = false; render(); resetAdminScroll(); } function resetAdminScroll() { window.requestAnimationFrame(() => { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); }); } function applySession(session) { if (Array.isArray(session?.permissionCatalog) && session.permissionCatalog.length) { state.permissionCatalog = session.permissionCatalog; } } function currentPermissions() { const session = getSession(); if (session?.user?.role === "master") { return state.permissionCatalog.map((permission) => permission.key); } const permissions = Array.isArray(session?.permissions) ? session.permissions : session?.user?.permissions; return Array.isArray(permissions) ? permissions : []; } function hasPermission(permission) { return currentPermissions().includes(permission); } function canAccessSection(section) { if (contentSectionKeys.includes(section)) return hasPermission(section); if (section === "users") return hasPermission("users"); return false; } function ensureAccessibleSection() { if (canAccessSection(state.section)) return; const fallback = ["work", "services", "technologies", "advanced", "users"].find(canAccessSection); state.section = fallback || ""; } function collection(section = state.section) { if (section === "advanced") return []; return state.collections[section] || []; } function selectedItem() { if (state.section === "advanced") return state.site; const items = collection(); const index = Math.min(Math.max(state.selected[state.section] || 0, 0), Math.max(items.length - 1, 0)); state.selected[state.section] = index; return items[index]; } function render() { ensureAccessibleSection(); const current = config[state.section]; els.tabs.forEach((tab) => { const canAccess = canAccessSection(tab.dataset.sectionTab); tab.hidden = !canAccess; tab.classList.toggle("is-active", canAccess && tab.dataset.sectionTab === state.section); }); if (els.leadsLink) els.leadsLink.hidden = !hasPermission("leads"); if (!current || !canAccessSection(state.section)) { renderNoAccess(); return; } els.sectionKicker.textContent = current.kicker; els.sectionTitle.textContent = current.title; els.sectionMeta.textContent = current.meta; els.save.hidden = false; els.addWork.hidden = !["work", "users"].includes(state.section); els.addWork.textContent = state.section === "users" ? "Add Sub Admin" : "Add Work"; els.save.textContent = state.section === "users" ? "Save User" : "Save Changes"; els.reset.hidden = state.section === "users"; els.export.hidden = state.section === "users"; els.importInput.closest(".admin-import-button").hidden = state.section === "users"; els.search.closest(".admin-search").hidden = state.section === "advanced"; els.search.placeholder = state.section === "users" ? "Search name, email, permissions" : "Search title, category, tags"; renderStats(); renderList(); renderEditor(); setStatus(state.dirty ? "Unsaved changes" : "Ready", false); } function renderStats() { const work = collection("work"); const services = collection("services"); const technologies = collection("technologies"); els.statWork.textContent = `${work.length} posts`; els.statServices.textContent = `${services.length} pages`; els.statTechnologies.textContent = `${technologies.length} posts`; if (els.statAdvanced) els.statAdvanced.textContent = `${advancedPanels.length} panels`; if (els.statUsers) els.statUsers.textContent = state.usersLoaded ? `${state.users.length} users` : "Access control"; } function renderNoAccess() { els.sectionKicker.textContent = "No Access"; els.sectionTitle.textContent = "This admin page is not available."; els.sectionMeta.textContent = hasPermission("leads") ? "This account can open the Lead Inbox." : "Ask the master admin to update this account's permissions."; els.addWork.hidden = true; els.save.hidden = true; els.reset.hidden = true; els.export.hidden = true; els.importInput.closest(".admin-import-button").hidden = true; els.search.closest(".admin-search").hidden = true; els.listCount.textContent = "0"; els.editorBadge.textContent = "Locked"; els.itemList.innerHTML = `<div class="admin-empty-state">No website content sections are enabled for this account.</div>`; els.editorForm.innerHTML = hasPermission("leads") ? `<div class="admin-empty-state"><a class="admin-primary-button" href="admin-leads">Open Lead Inbox</a></div>` : `<div class="admin-empty-state">Contact the master admin for access.</div>`; setStatus("Access limited", true); } function renderList() { if (state.section === "users") { renderUsersList(); return; } if (state.section === "advanced") { renderAdvancedList(); return; } const items = collection(); const query = state.query; const filtered = items .map((item, index) => ({ item, index })) .filter(({ item }) => { if (!query) return true; return [ item.title, item.label, item.category, item.summary, item.description, ...(item.tags || []), ...(item.tools || []) ].join(" ").toLowerCase().includes(query); }); els.listCount.textContent = String(filtered.length); if (filtered.length === 0) { els.itemList.innerHTML = `<div class="admin-empty-state">No matching content.</div>`; return; } els.itemList.innerHTML = filtered.map(({ item, index }) => { const active = index === state.selected[state.section]; const badges = buildListBadges(item); const title = item.title || item.label || "Untitled"; const meta = [item.category, item.slug || item.id].filter(Boolean).join(" / "); return ` <button class="${active ? "is-active" : ""}" type="button" data-item-index="${index}"> <strong>${esc(title)}</strong> <span>${esc(meta)}</span> ${badges} </button> `; }).join(""); } function buildListBadges(item) { const badges = []; if (item.featured) badges.push("Featured"); if (item.showOnHome) badges.push("Home"); if (item.tags?.length) badges.push(`${item.tags.length} tags`); if (item.tools?.length) badges.push(`${item.tools.length} tools`); if (badges.length === 0) return ""; return `<em>${badges.map(esc).join(" / ")}</em>`; } function renderEditor() { if (state.section === "users") { renderUsersEditor(); return; } if (state.section === "advanced") { renderAdvancedEditor(); return; } const item = selectedItem(); if (!item) { els.editorForm.innerHTML = `<div class="admin-empty-state">Select or add content to edit.</div>`; return; } const current = config[state.section]; els.editorBadge.textContent = state.dirty ? "Unsaved" : "Draft"; if (state.section === "work") { els.editorForm.innerHTML = renderWorkEditor(item, current.idField); return; } if (state.section === "technologies") { els.editorForm.innerHTML = renderTechnologyEditor(item, current.idField); return; } els.editorForm.innerHTML = renderServiceEditor(item, current.idField); } function renderAdvancedList() { els.listCount.textContent = String(advancedPanels.length); els.itemList.innerHTML = advancedPanels.map((panel, index) => ` <button class="${index === state.selected.advanced ? "is-active" : ""}" type="button" data-item-index="${index}"> <strong>${esc(panel.title)}</strong> <span>${esc(panel.meta)}</span> </button> `).join(""); } function renderAdvancedEditor() { const panel = advancedPanels[state.selected.advanced] || advancedPanels[0]; els.editorBadge.textContent = state.dirty ? "Unsaved" : "Site"; if (panel.id === "video") { els.editorForm.innerHTML = renderVideoSettingsEditor(); return; } if (panel.id === "about") { els.editorForm.innerHTML = renderAboutSettingsEditor(); return; } if (panel.id === "industries") { els.editorForm.innerHTML = renderIndustriesSettingsEditor(); return; } if (panel.id === "contact") { els.editorForm.innerHTML = renderContactSettingsEditor(); return; } if (panel.id === "privacy") { els.editorForm.innerHTML = renderPrivacyPolicySettingsEditor(); return; } els.editorForm.innerHTML = renderHomepageSettingsEditor(); } function renderUsersList() { if (!state.usersLoaded) { els.listCount.textContent = "0"; els.itemList.innerHTML = `<div class="admin-empty-state">Loading admin users...</div>`; loadAdminUsers(); return; } const query = state.query; const filtered = state.users .map((user, index) => ({ user, index })) .filter(({ user }) => { if (!query) return true; return [ user.name, user.email, user.role, ...(user.permissions || []) ].join(" ").toLowerCase().includes(query); }); els.listCount.textContent = String(filtered.length); if (filtered.length === 0) { els.itemList.innerHTML = `<div class="admin-empty-state">No matching admin users.</div>`; return; } if (!filtered.some(({ index }) => index === state.selected.users)) { state.selected.users = filtered[0].index; } els.itemList.innerHTML = filtered.map(({ user, index }) => { const active = index === state.selected.users; const permissionCount = user.role === "master" ? state.permissionCatalog.length : (user.permissions || []).length; const status = user.enabled === false ? "Disabled" : `${permissionCount} permissions`; return ` <button class="${active ? "is-active" : ""}" type="button" data-item-index="${index}"> <strong>${esc(user.name || "Unnamed admin")}</strong> <span>${esc(user.email || "Email required")}</span> <em>${esc(user.role === "master" ? "Master admin" : status)}</em> </button> `; }).join(""); } function renderUsersEditor() { if (!state.usersLoaded) { els.editorBadge.textContent = "Loading"; els.editorForm.innerHTML = `<div class="admin-empty-state">Loading admin user access...</div>`; loadAdminUsers(); return; } const user = selectedAdminUser(); if (!user) { els.editorBadge.textContent = "Users"; els.editorForm.innerHTML = `<div class="admin-empty-state">Add a sub-admin to manage access.</div>`; return; } const isMaster = user.role === "master"; els.editorBadge.textContent = isMaster ? "Master" : (user.isDraft ? "New" : "Sub Admin"); els.editorForm.innerHTML = ` <div class="admin-editor-section"> <h3>${esc(isMaster ? "Master Admin Login" : "Sub Admin Login")}</h3> ${renderTextField("Name", "name", user.name)} ${renderTextField("Email", "email", user.email, { hint: "This is the login email for this admin." })} ${renderPasswordField(user)} ${isMaster ? `<small class="admin-note">Master admins always keep access to every section.</small>` : ` <div class="admin-switch-row"> ${renderSwitch("Account enabled", "enabled", user.enabled !== false)} </div> `} </div> <div class="admin-editor-section"> <h3>Section Access</h3> <div class="admin-permission-grid"> ${renderPermissionSwitches(user)} </div> </div> ${isMaster ? "" : ` <div class="admin-editor-actions"> <button class="admin-danger-button" type="button" data-delete-user>Delete Sub Admin</button> </div> `} `; } function renderPasswordField(user) { const hint = user.isDraft ? "Required for a new sub-admin. Use at least 6 characters." : "Leave blank to keep the current password."; return ` <label class="admin-field"> <span>Password</span> <input type="password" name="password" value="${esc(user.password || "")}" autocomplete="new-password"> <small>${esc(hint)}</small> </label> `; } function renderPermissionSwitches(user) { const enabled = user.role === "master" ? state.permissionCatalog.map((permission) => permission.key) : (user.permissions || []); return state.permissionCatalog.map((permission) => { const checked = enabled.includes(permission.key); const disabled = user.role === "master" ? " disabled" : ""; return ` <label class="admin-switch admin-permission-switch"> <input type="checkbox" name="permission-${esc(permission.key)}" data-user-permission="${esc(permission.key)}" ${checked ? "checked" : ""}${disabled}> <span></span> <strong>${esc(permission.label)}</strong> </label> `; }).join(""); } function renderHomepageSettingsEditor() { const site = state.site; return ` <div class="admin-editor-section"> <h3>Hero</h3> ${renderTextField("Eyebrow", "home.hero.eyebrow", site.home.hero.eyebrow)} <div class="admin-field-grid"> ${renderTextField("Headline Main", "home.hero.headlineMain", site.home.hero.headlineMain)} ${renderTextField("Headline Accent", "home.hero.headlineAccent", site.home.hero.headlineAccent)} </div> ${renderTextarea("Rotating Headline Words", "home.hero.rotatingWords", cms.textFromList(site.home.hero.rotatingWords), 4, "list")} ${renderTextarea("Hero Description", "home.hero.description", site.home.hero.description, 4)} <div class="admin-field-grid"> ${renderTextField("Primary Button", "home.hero.primaryButton", site.home.hero.primaryButton)} ${renderTextField("Video Button", "home.hero.videoButton", site.home.hero.videoButton)} </div> ${renderTextField("Capability Label", "home.hero.trustLabel", site.home.hero.trustLabel)} ${renderTextarea("Capability Chips", "home.hero.capabilities", cms.textFromList(site.home.hero.capabilities), 4, "list")} </div> <div class="admin-editor-section"> <h3>Stats</h3> ${site.home.stats.map((stat, index) => renderStatEditor(stat, index)).join("")} </div> <div class="admin-editor-section"> <h3>Homepage Method Showcase</h3> ${renderTextField("Section Kicker", "home.method.kicker", site.home.method.kicker)} ${renderTextarea("Section Title", "home.method.title", site.home.method.title, 3)} ${site.home.method.steps.map((step, index) => renderMethodStepEditor(step, index)).join("")} </div> <div class="admin-editor-section"> <h3>Homepage About Video</h3> ${renderTextField("Section Kicker", "home.aboutVideo.kicker", site.home.aboutVideo.kicker)} ${renderTextField("Section Title", "home.aboutVideo.title", site.home.aboutVideo.title)} ${renderTextarea("Section Summary", "home.aboutVideo.summary", site.home.aboutVideo.summary, 3)} <div class="admin-field-grid"> ${renderTextField("Card Title", "home.aboutVideo.cardTitle", site.home.aboutVideo.cardTitle)} ${renderTextField("Card Subtitle", "home.aboutVideo.cardSubtitle", site.home.aboutVideo.cardSubtitle)} </div> ${renderTextField("Card Image URL", "home.aboutVideo.image", site.home.aboutVideo.image)} </div> <div class="admin-editor-section"> <h3>Industries and Work</h3> ${renderTextField("Industries Kicker", "home.industries.kicker", site.home.industries.kicker)} ${renderTextarea("Industries Title", "home.industries.title", site.home.industries.title, 3)} ${renderTextarea("Industry Chips", "home.industries.items", cms.textFromList(site.home.industries.items), 4, "list")} ${renderTextField("Work Kicker", "home.work.kicker", site.home.work.kicker)} ${renderTextarea("Work Title", "home.work.title", site.home.work.title, 3)} <div class="admin-field-grid"> ${renderTextField("More Work Button", "home.work.moreButton", site.home.work.moreButton)} ${renderTextField("More Work Link", "home.work.moreHref", site.home.work.moreHref)} </div> </div> <div class="admin-editor-section"> <h3>Homepage Contact CTA</h3> ${renderTextField("Contact Kicker", "home.contact.kicker", site.home.contact.kicker)} ${renderTextarea("Contact Title", "home.contact.title", site.home.contact.title, 3)} ${renderTextField("Contact Button", "home.contact.button", site.home.contact.button)} </div> `; } function renderMethodStepEditor(step, index) { return ` <div class="admin-nested-editor"> <strong>Step ${index + 1}</strong> <div class="admin-field-grid admin-field-grid-three"> ${renderTextField("Label", `home.method.steps.${index}.label`, step.label)} ${renderColorField("Accent Color", `home.method.steps.${index}.color`, step.color)} ${renderSelectField("Art Style", `home.method.steps.${index}.art`, step.art, [ ["strategy", "Strategy"], ["design", "Design"], ["build", "AI Build"], ["automate", "Automate"], ["scale", "Scale"] ])} </div> ${renderTextarea("Card Description", `home.method.steps.${index}.description`, step.description, 3)} ${renderTextarea("Bullet Points", `home.method.steps.${index}.items`, cms.textFromList(step.items), 5, "list")} </div> `; } function renderVideoSettingsEditor() { const video = state.site.video; return ` <div class="admin-editor-section"> <h3>Overview Video</h3> ${renderTextField("Video Link", "video.sourceUrl", video.sourceUrl, { hint: "Paste YouTube, Vimeo, MP4, WebM, or an uploaded short video." })} <label class="admin-upload-control"> <span>Upload Short Video</span> <input type="file" accept="video/*" data-site-video-upload> </label> ${renderTextField("Poster Image URL", "video.poster", video.poster)} ${renderTextField("Video Eyebrow", "video.eyebrow", video.eyebrow)} ${renderTextField("Video Title", "video.title", video.title)} ${renderTextarea("Video Description", "video.description", video.description, 4)} ${renderTextField("Player Label", "video.playerLabel", video.playerLabel)} </div> `; } function renderAboutSettingsEditor() { const about = state.site.about; return ` <div class="admin-editor-section"> <h3>About Hero</h3> ${renderTextField("Hero Kicker", "about.heroKicker", about.heroKicker)} ${renderTextField("Hero Title", "about.heroTitle", about.heroTitle)} ${renderTextarea("Hero Summary", "about.heroSummary", about.heroSummary, 4)} ${renderSiteImageEditor("Hero Video Image", "about.videoImage", about.videoImage)} <div class="admin-field-grid"> ${renderTextField("Video Small Label", "about.videoSmall", about.videoSmall)} ${renderTextField("Video Card Title", "about.videoTitle", about.videoTitle)} </div> </div> <div class="admin-editor-section"> <h3>Mission and Vision</h3> ${renderTextField("Mission Label", "about.missionTitle", about.missionTitle)} ${renderTextarea("Mission Text", "about.missionText", about.missionText, 4)} ${renderTextField("Vision Label", "about.visionTitle", about.visionTitle)} ${renderTextarea("Vision Text", "about.visionText", about.visionText, 4)} </div> <div class="admin-editor-section"> <h3>Principles</h3> ${renderTextField("Principles Kicker", "about.principlesKicker", about.principlesKicker)} ${renderTextField("Principles Title", "about.principlesTitle", about.principlesTitle)} ${renderTextarea("Principles Text", "about.principlesText", about.principlesText, 4)} ${renderSiteImageEditor("Principles Circle Image", "about.principlesImage", about.principlesImage)} ${about.principleItems.map((item, index) => renderTitleTextItemEditor(item, index, "about.principleItems", "Principle")).join("")} </div> <div class="admin-editor-section"> <h3>Pillars</h3> ${renderTextField("Pillars Kicker", "about.pillarsKicker", about.pillarsKicker)} ${renderTextField("Pillars Title", "about.pillarsTitle", about.pillarsTitle)} ${renderTextarea("Pillars Text", "about.pillarsText", about.pillarsText, 4)} ${renderSiteImageEditor("Pillars Circle Image", "about.pillarsImage", about.pillarsImage)} ${about.pillarItems.map((item, index) => renderTitleTextItemEditor(item, index, "about.pillarItems", "Pillar")).join("")} </div> <div class="admin-editor-section"> <h3>Recognition and CTA</h3> ${renderTextField("Recognition Kicker", "about.recognitionKicker", about.recognitionKicker)} ${renderTextField("Recognition Title", "about.recognitionTitle", about.recognitionTitle)} ${about.metrics.map((metric, index) => renderMetricItemEditor(metric, index, "about.metrics", "Metric")).join("")} ${renderTextField("CTA Kicker", "about.ctaKicker", about.ctaKicker)} ${renderTextField("CTA Title", "about.ctaTitle", about.ctaTitle)} ${renderTextField("CTA Button", "about.ctaButton", about.ctaButton)} </div> `; } function renderIndustriesSettingsEditor() { const industries = state.site.industries; return ` <div class="admin-editor-section"> <h3>Industry Hero</h3> ${renderTextField("Hero Kicker", "industries.heroKicker", industries.heroKicker)} ${renderTextField("Hero Title", "industries.heroTitle", industries.heroTitle)} ${renderTextarea("Hero Summary", "industries.heroSummary", industries.heroSummary, 4)} ${renderSiteImageEditor("Hero Image", "industries.heroImage", industries.heroImage)} <div class="admin-field-grid"> ${renderTextField("Primary Button", "industries.heroPrimaryButton", industries.heroPrimaryButton)} ${renderTextField("Secondary Button", "industries.heroSecondaryButton", industries.heroSecondaryButton)} </div> <div class="admin-field-grid"> ${renderTextField("Command Label", "industries.commandLabel", industries.commandLabel)} ${renderTextField("Command Value", "industries.commandValue", industries.commandValue)} ${renderTextField("Route Label", "industries.routeLabel", industries.routeLabel)} ${renderTextField("Route Value", "industries.routeValue", industries.routeValue)} </div> </div> <div class="admin-editor-section"> <h3>Snapshot Stats</h3> ${industries.stats.map((stat, index) => renderMetricItemEditor(stat, index, "industries.stats", "Stat")).join("")} </div> <div class="admin-editor-section"> <h3>Industry Cards</h3> ${renderTextField("Section Kicker", "industries.gridKicker", industries.gridKicker)} ${renderTextarea("Section Title", "industries.gridTitle", industries.gridTitle, 3)} ${industries.cards.map((card, index) => renderIndustryCardEditor(card, index)).join("")} </div> <div class="admin-editor-section"> <h3>Dispatch Section</h3> ${renderTextField("Dispatch Kicker", "industries.dispatchKicker", industries.dispatchKicker)} ${renderTextField("Dispatch Title", "industries.dispatchTitle", industries.dispatchTitle)} ${renderTextarea("Dispatch Text", "industries.dispatchText", industries.dispatchText, 4)} ${renderTextField("Dispatch Button", "industries.dispatchButton", industries.dispatchButton)} <div class="admin-field-grid"> ${renderTextField("Console Label", "industries.dispatchConsoleLabel", industries.dispatchConsoleLabel)} ${renderTextField("Console Status", "industries.dispatchConsoleStatus", industries.dispatchConsoleStatus)} </div> ${industries.dispatchQueue.map((item, index) => renderDispatchItemEditor(item, index)).join("")} </div> <div class="admin-editor-section"> <h3>System Cards</h3> ${renderTextField("Section Kicker", "industries.systemKicker", industries.systemKicker)} ${renderTextarea("Section Title", "industries.systemTitle", industries.systemTitle, 3)} ${industries.systemItems.map((item, index) => renderNumberedTextItemEditor(item, index, "industries.systemItems", "System Card")).join("")} </div> <div class="admin-editor-section"> <h3>Industry CTA</h3> ${renderTextField("CTA Kicker", "industries.ctaKicker", industries.ctaKicker)} ${renderTextarea("CTA Title", "industries.ctaTitle", industries.ctaTitle, 3)} ${renderTextField("CTA Button", "industries.ctaButton", industries.ctaButton)} </div> `; } function renderContactSettingsEditor() { const footer = state.site.footer; const contact = state.site.contactModal; const social = footer.socialLinks || {}; return ` <div class="admin-editor-section"> <h3>Contact Modal</h3> ${renderTextField("Contact Kicker", "contactModal.kicker", contact.kicker)} ${renderTextField("Contact Title", "contactModal.title", contact.title)} ${renderTextarea("Contact Description", "contactModal.description", contact.description, 4)} ${renderTextField("Submit Button", "contactModal.submitButton", contact.submitButton)} </div> <div class="admin-editor-section"> <h3>Footer</h3> ${renderTextField("Footer CTA Title", "footer.ctaTitle", footer.ctaTitle)} ${renderTextField("Footer CTA Button", "footer.ctaButton", footer.ctaButton)} ${renderTextField("Subscribe Title", "footer.subscribeTitle", footer.subscribeTitle)} ${renderTextarea("Subscribe Text", "footer.subscribeText", footer.subscribeText, 3)} <div class="admin-field-grid"> ${renderTextField("Email", "footer.email", footer.email)} ${renderTextField("Phone", "footer.phone", footer.phone)} </div> </div> <div class="admin-editor-section"> <h3>Follow Us Icons</h3> ${renderTextField("Follow Title", "footer.followTitle", footer.followTitle)} <div class="admin-field-grid"> ${renderTextField("X Link", "footer.socialLinks.x", social.x, { hint: "Leave blank to hide this icon." })} ${renderTextField("LinkedIn Link", "footer.socialLinks.linkedin", social.linkedin, { hint: "Leave blank to hide this icon." })} ${renderTextField("Facebook Link", "footer.socialLinks.facebook", social.facebook, { hint: "Leave blank to hide this icon." })} ${renderTextField("YouTube Link", "footer.socialLinks.youtube", social.youtube, { hint: "Leave blank to hide this icon." })} ${renderTextField("Phone Icon Link", "footer.socialLinks.phone", social.phone, { hint: "Blank uses the footer phone number." })} </div> </div> `; } function renderPrivacyPolicySettingsEditor() { const policy = state.site.privacyPolicy; return ` <div class="admin-editor-section"> <h3>Privacy Page Hero</h3> ${renderTextField("Kicker", "privacyPolicy.kicker", policy.kicker)} ${renderTextField("Page Title", "privacyPolicy.title", policy.title)} ${renderTextField("Updated Label", "privacyPolicy.updatedLabel", policy.updatedLabel)} ${renderTextarea("Intro Text", "privacyPolicy.intro", policy.intro, 4)} </div> <div class="admin-editor-section"> <h3>Policy Sections</h3> ${policy.sections.map((section, index) => renderTitleTextItemEditor(section, index, "privacyPolicy.sections", "Policy Section")).join("")} </div> <div class="admin-editor-section"> <h3>Privacy Contact Note</h3> ${renderTextField("Contact Heading", "privacyPolicy.contactTitle", policy.contactTitle)} ${renderTextarea("Contact Text", "privacyPolicy.contactText", policy.contactText, 3)} </div> `; } function renderStatEditor(stat, index) { return ` <div class="admin-nested-editor"> <strong>Stat ${index + 1}</strong> <div class="admin-field-grid admin-field-grid-three"> ${renderTextField("Number", `home.stats.${index}.value`, stat.value)} ${renderTextField("Suffix", `home.stats.${index}.suffix`, stat.suffix)} ${renderTextField("Label", `home.stats.${index}.label`, stat.label)} </div> </div> `; } function renderTitleTextItemEditor(item, index, basePath, label) { return ` <div class="admin-nested-editor"> <strong>${esc(label)} ${index + 1}</strong> ${renderTextField("Title", `${basePath}.${index}.title`, item.title)} ${renderTextarea("Text", `${basePath}.${index}.text`, item.text, 3)} </div> `; } function renderNumberedTextItemEditor(item, index, basePath, label) { return ` <div class="admin-nested-editor"> <strong>${esc(label)} ${index + 1}</strong> <div class="admin-field-grid"> ${renderTextField("Number", `${basePath}.${index}.number`, item.number)} ${renderTextField("Title", `${basePath}.${index}.title`, item.title)} </div> ${renderTextarea("Text", `${basePath}.${index}.text`, item.text, 3)} </div> `; } function renderMetricItemEditor(metric, index, basePath, label) { return ` <div class="admin-nested-editor"> <strong>${esc(label)} ${index + 1}</strong> <div class="admin-field-grid"> ${renderTextField("Value", `${basePath}.${index}.value`, metric.value)} ${renderTextField("Label", `${basePath}.${index}.label`, metric.label)} </div> </div> `; } function renderIndustryCardEditor(card, index) { return ` <div class="admin-nested-editor"> <strong>Industry Card ${index + 1}</strong> <div class="admin-field-grid"> ${renderTextField("Badge", `industries.cards.${index}.badge`, card.badge)} ${renderTextField("Title", `industries.cards.${index}.title`, card.title)} </div> ${renderTextarea("Description", `industries.cards.${index}.text`, card.text, 3)} ${renderTextarea("Tags", `industries.cards.${index}.tags`, cms.textFromList(card.tags), 3, "list")} </div> `; } function renderDispatchItemEditor(item, index) { return ` <div class="admin-nested-editor"> <strong>Dispatch Queue ${index + 1}</strong> <div class="admin-field-grid admin-field-grid-three"> ${renderTextField("Number", `industries.dispatchQueue.${index}.number`, item.number)} ${renderTextField("Title", `industries.dispatchQueue.${index}.title`, item.title)} ${renderTextField("Status", `industries.dispatchQueue.${index}.status`, item.status)} </div> </div> `; } function renderServiceEditor(item, idField) { const insightCards = getServiceInsightCardsForEditor(item); return ` ${renderTextField("Slug", idField, item[idField], { readonly: true, hint: "Stable page link" })} ${renderTextField("Label", "label", item.label)} ${renderTextField("Category", "category", item.category)} ${renderTextField("Page Title", "title", item.title)} ${renderTextarea("Summary", "summary", item.summary, 4)} ${renderImageEditor(item)} ${renderTextarea("Focus Points", "focus", cms.textFromList(item.focus), 6, "list")} ${renderTextarea("Outcomes", "outcomes", cms.textFromList(item.outcomes), 5, "list")} ${renderTextarea("Tools", "tools", cms.textFromList(item.tools), 4, "list")} <div class="admin-editor-section"> <h3>Service Insights Cards</h3> ${renderTextField("Insights Heading", "insightTitle", item.insightTitle || `${item.label} image and content direction`)} ${insightCards.map((card, index) => renderServiceInsightCardEditor(card, index)).join("")} </div> `; } function renderServiceInsightCardEditor(card, index) { const imagePath = `insightCards.${index}.image`; return ` <div class="admin-nested-editor"> <strong>Insight Card ${index + 1}</strong> <div class="admin-field-grid"> ${renderTextField("Eyebrow", `insightCards.${index}.eyebrow`, card.eyebrow)} ${renderTextField("Title", `insightCards.${index}.title`, card.title)} </div> ${renderTextarea("Text", `insightCards.${index}.text`, card.text, 3)} <div class="admin-image-editor admin-card-image-editor"> <div class="admin-image-fields"> <div class="admin-field-grid"> ${renderTextField("Image URL", `${imagePath}.src`, card.image.src)} ${renderTextField("Image Position", `${imagePath}.position`, card.image.position)} </div> <label class="admin-upload-control"> <span>Upload Insight Card ${index + 1} Image</span> <input type="file" accept="image/*" data-insight-image-upload="${esc(imagePath)}"> </label> </div> <figure> <img src="${esc(card.image.src)}" alt="" data-image-preview="${esc(imagePath)}"> </figure> </div> </div> `; } function getServiceInsightCardsForEditor(item) { const defaults = buildDefaultServiceInsightCardsForEditor(item); if (Array.isArray(item.insightCards) && item.insightCards.length) { return defaults.map((fallback, index) => ( normalizeServiceInsightCardForEditor(item.insightCards[index] || fallback, index, item) )); } return defaults; } function buildDefaultServiceInsightCardsForEditor(item) { const services = state.collections.services || []; const serviceIndex = Math.max(0, services.findIndex((service) => service.slug === item.slug)); const count = Math.max(services.length, 1); return [0, 7, 14].map((offset, index) => { const related = services[(serviceIndex + offset) % count] || item; return normalizeServiceInsightCardForEditor({ eyebrow: `Direction ${index + 1}`, title: index === 0 ? item.label : related.label, text: index === 0 ? item.summary : related.summary, image: related.image }, index, item); }); } function prepareServiceInsightCards(item) { item.insightCards = getServiceInsightCardsForEditor(item); return item.insightCards; } function prepareCurrentInsightCards(item) { if (state.section === "technologies") { item.insightCards = getTechnologyInsightCardsForEditor(item); return item.insightCards; } return prepareServiceInsightCards(item); } function normalizeServiceInsightCardForEditor(card, index, item) { const fallbackImage = cms.normalizeImage(item.image); const sourceImage = typeof card?.image === "string" ? { src: card.image, position: "center" } : card?.image || fallbackImage; const image = cms.normalizeImage(sourceImage); return { eyebrow: String(card?.eyebrow || `Direction ${index + 1}`), title: String(card?.title || item.label || "Service Direction"), text: String(card?.text || card?.summary || item.summary || ""), image: { src: image.src || fallbackImage.src, position: image.position || fallbackImage.position || "center" } }; } function renderTechnologyEditor(item, idField) { const insightCards = getTechnologyInsightCardsForEditor(item); return ` ${renderTextField("Slug", idField, item[idField], { readonly: true, hint: "Stable page link" })} ${renderTextField("Label", "label", item.label)} ${renderTextField("Category", "category", item.category)} ${renderTextField("Page Title", "title", item.title)} ${renderTextarea("Summary", "summary", item.summary, 4)} ${renderImageEditor(item)} ${renderTextarea("Signals", "signals", cms.textFromList(item.signals), 4, "list")} ${renderTextarea("Use Cases", "focus", cms.textFromList(item.focus), 6, "list")} ${renderTextarea("Outcomes", "outcomes", cms.textFromList(item.outcomes), 5, "list")} ${renderTextarea("Tools", "tools", cms.textFromList(item.tools), 4, "list")} <div class="admin-editor-section"> <h3>Technology Insight Cards</h3> ${renderTextField("Insights Heading", "insightTitle", item.insightTitle || `${item.label} stack and content direction`)} ${insightCards.map((card, index) => renderTechnologyInsightCardEditor(card, index)).join("")} </div> `; } function renderTechnologyInsightCardEditor(card, index) { const imagePath = `insightCards.${index}.image`; return ` <div class="admin-nested-editor"> <strong>Stack Card ${index + 1}</strong> <div class="admin-field-grid"> ${renderTextField("Eyebrow", `insightCards.${index}.eyebrow`, card.eyebrow)} ${renderTextField("Title", `insightCards.${index}.title`, card.title)} </div> ${renderTextarea("Text", `insightCards.${index}.text`, card.text, 3)} <div class="admin-image-editor admin-card-image-editor"> <div class="admin-image-fields"> <div class="admin-field-grid"> ${renderTextField("Image URL", `${imagePath}.src`, card.image.src)} ${renderTextField("Image Position", `${imagePath}.position`, card.image.position)} </div> <label class="admin-upload-control"> <span>Upload Stack Card ${index + 1} Image</span> <input type="file" accept="image/*" data-insight-image-upload="${esc(imagePath)}"> </label> </div> <figure> <img src="${esc(card.image.src)}" alt="" data-image-preview="${esc(imagePath)}"> </figure> </div> </div> `; } function getTechnologyInsightCardsForEditor(item) { const defaults = buildDefaultTechnologyInsightCardsForEditor(item); if (Array.isArray(item.insightCards) && item.insightCards.length) { return defaults.map((fallback, index) => ( normalizeTechnologyInsightCardForEditor(item.insightCards[index] || fallback, index, item) )); } return defaults; } function buildDefaultTechnologyInsightCardsForEditor(item) { const technologies = state.collections.technologies || []; const technologyIndex = Math.max(0, technologies.findIndex((technology) => technology.slug === item.slug)); const count = Math.max(technologies.length, 1); return [0, 5, 13].map((offset, index) => { const related = technologies[(technologyIndex + offset) % count] || item; return normalizeTechnologyInsightCardForEditor({ eyebrow: `Stack Direction ${index + 1}`, title: index === 0 ? item.label : related.label, text: index === 0 ? item.summary : related.summary, image: related.image }, index, item); }); } function normalizeTechnologyInsightCardForEditor(card, index, item) { const fallbackImage = cms.normalizeImage(item.image); const sourceImage = typeof card?.image === "string" ? { src: card.image, position: "center" } : card?.image || fallbackImage; const image = cms.normalizeImage(sourceImage); return { eyebrow: String(card?.eyebrow || `Stack Direction ${index + 1}`), title: String(card?.title || item.label || "Technology Direction"), text: String(card?.text || card?.summary || item.summary || ""), image: { src: image.src || fallbackImage.src, position: image.position || fallbackImage.position || "center" } }; } function renderWorkEditor(item, idField) { return ` ${renderTextField("Post ID", idField, item[idField])} ${renderTextField("Title", "title", item.title)} ${renderTextField("Category", "category", item.category)} ${renderTextarea("Description", "description", item.description, 5)} ${renderImageEditor(item)} ${renderTextarea("Tags", "tags", cms.textFromList(item.tags), 4, "list")} <div class="admin-switch-row"> ${renderSwitch("Featured on Work page", "featured", item.featured)} ${renderSwitch("Show on homepage", "showOnHome", item.showOnHome)} </div> <div class="admin-editor-actions"> <button class="admin-ghost-button" type="button" data-duplicate-work>Duplicate</button> <button class="admin-danger-button" type="button" data-delete-work>Delete</button> </div> `; } function renderTextField(label, name, value, options = {}) { const readonly = options.readonly ? " readonly" : ""; const hint = options.hint ? `<small>${esc(options.hint)}</small>` : ""; return ` <label class="admin-field"> <span>${esc(label)}</span> <input type="text" name="${esc(name)}" value="${esc(value)}"${readonly}> ${hint} </label> `; } function renderColorField(label, name, value) { return ` <label class="admin-field"> <span>${esc(label)}</span> <input type="color" name="${esc(name)}" value="${esc(value || "#3f91a6")}"> </label> `; } function renderSelectField(label, name, value, options) { return ` <label class="admin-field"> <span>${esc(label)}</span> <select name="${esc(name)}"> ${(options || []).map(([optionValue, optionLabel]) => ` <option value="${esc(optionValue)}" ${String(value) === String(optionValue) ? "selected" : ""}>${esc(optionLabel)}</option> `).join("")} </select> </label> `; } function renderTextarea(label, name, value, rows, listType) { return ` <label class="admin-field"> <span>${esc(label)}</span> <textarea name="${esc(name)}" rows="${rows}" ${listType ? `data-list-field="${listType}"` : ""}>${esc(value)}</textarea> </label> `; } function renderImageEditor(item) { const image = cms.normalizeImage(item.image); return ` <div class="admin-image-editor"> <div class="admin-image-fields"> ${renderTextField("Image URL", "image.src", image.src)} ${renderTextField("Image Position", "image.position", image.position)} <label class="admin-upload-control"> <span>Upload Image</span> <input type="file" accept="image/*" data-image-upload> </label> </div> <figure> <img src="${esc(image.src)}" alt="" data-image-preview> </figure> </div> `; } function renderSiteImageEditor(label, path, value) { const image = normalizeAdminImage(value, path); return ` <div class="admin-image-editor"> <div class="admin-image-fields"> ${renderTextField(`${label} URL`, `${path}.src`, image.src)} ${renderTextField(`${label} Position`, `${path}.position`, image.position)} <label class="admin-upload-control"> <span>Upload ${esc(label)}</span> <input type="file" accept="image/*" data-site-image-upload="${esc(path)}"> </label> </div> <figure> <img src="${esc(image.src)}" alt="" data-image-preview="${esc(path)}"> </figure> </div> `; } function renderSwitch(label, name, checked) { return ` <label class="admin-switch"> <input type="checkbox" name="${esc(name)}" ${checked ? "checked" : ""}> <span></span> <strong>${esc(label)}</strong> </label> `; } function handleEditorInput(event) { const target = event.target; if (!target.name) return; if (state.section === "users") { handleUserEditorInput(target); return; } const item = selectedItem(); if (!item) return; if ((state.section === "services" || state.section === "technologies") && target.name.startsWith("insightCards.")) { prepareCurrentInsightCards(item); } const value = target.dataset.listField ? cms.listify(target.value) : target.value; setNestedValue(item, target.name, value); if (target.name.endsWith(".src")) { updateImagePreview(target.value, target.name.slice(0, -4)); } markDirty(); renderList(); } function handleEditorChange(event) { const target = event.target; if (state.section === "users") { handleUserEditorChange(target); return; } const item = selectedItem(); if (!item) return; if (state.section === "advanced" && target.matches("[data-site-video-upload]")) { loadUploadedSiteVideo(target.files?.[0]); return; } if (state.section === "advanced" && target.matches("[data-site-image-upload]")) { loadUploadedSiteImage(target.files?.[0], target.dataset.siteImageUpload); return; } if (target.matches("[data-insight-image-upload]")) { loadUploadedNestedImage(target.files?.[0], item, target.dataset.insightImageUpload); return; } if (target.matches("[data-image-upload]")) { loadUploadedImage(target.files?.[0], item); return; } if (target.type === "checkbox" && target.name) { if (state.section === "work" && target.name === "featured" && target.checked) { collection("work").forEach((workItem) => { workItem.featured = false; }); } setNestedValue(item, target.name, target.checked); markDirty(); render(); } } function handleEditorClick(event) { if (event.target.closest("[data-delete-user]")) { deleteAdminUser(); return; } if (event.target.closest("[data-duplicate-work]")) { duplicateWorkItem(); return; } if (event.target.closest("[data-delete-work]")) { deleteWorkItem(); } } function handleUserEditorInput(target) { const user = selectedAdminUser(); if (!user || target.type === "checkbox") return; if (["name", "email", "password"].includes(target.name)) { user[target.name] = target.value; markDirty(); renderList(); } } function handleUserEditorChange(target) { const user = selectedAdminUser(); if (!user || user.role === "master") return; if (target.name === "enabled") { user.enabled = target.checked; markDirty(); renderList(); return; } if (target.dataset.userPermission) { const permissions = new Set(user.permissions || []); if (target.checked) { permissions.add(target.dataset.userPermission); } else { permissions.delete(target.dataset.userPermission); } user.permissions = Array.from(permissions); markDirty(); renderList(); } } function setNestedValue(item, name, value) { if (!name.includes(".")) { item[name] = value; return; } const parts = name.split("."); let target = item; parts.slice(0, -1).forEach((part, index) => { const expectsArray = /^\d+$/.test(parts[index + 1]); if (!target[part] || typeof target[part] !== "object") { target[part] = expectsArray ? [] : {}; } if (expectsArray && !Array.isArray(target[part])) { const previous = target[part]; target[part] = []; Object.keys(previous).forEach((key) => { target[part][key] = previous[key]; }); } target = target[part]; }); target[parts[parts.length - 1]] = value; } function getNestedValue(item, name) { return String(name || "").split(".").reduce((target, part) => target?.[part], item); } function normalizeSiteSettings(value) { const site = deepMerge(defaultSiteSettings, value && typeof value === "object" ? value : {}); site.home.hero.rotatingWords = cms.listify(site.home.hero.rotatingWords); site.home.hero.capabilities = cms.listify(site.home.hero.capabilities); site.home.industries.items = cms.listify(site.home.industries.items); site.home.stats = normalizeStats(site.home.stats); site.home.method = normalizeMethodSettings(site.home.method); site.about = normalizeAboutSettings(site.about); site.industries = normalizeIndustrySettings(site.industries); site.privacyPolicy = normalizePrivacyPolicySettings(site.privacyPolicy); return site; } function normalizeStats(stats) { const source = Array.isArray(stats) && stats.length ? stats : defaultSiteSettings.home.stats; return source.slice(0, 4).map((stat, index) => ({ value: String(stat?.value ?? defaultSiteSettings.home.stats[index]?.value ?? "0"), suffix: String(stat?.suffix ?? defaultSiteSettings.home.stats[index]?.suffix ?? ""), label: String(stat?.label ?? defaultSiteSettings.home.stats[index]?.label ?? "Metric") })); } function normalizeMethodSettings(method) { const defaults = defaultSiteSettings.home.method; const source = method && typeof method === "object" ? method : {}; const savedSteps = Array.isArray(source.steps) ? source.steps : []; return { kicker: String(source.kicker ?? defaults.kicker), title: String(source.title ?? defaults.title), steps: defaults.steps.map((defaultStep, index) => { const savedStep = savedSteps.find((step) => step?.key === defaultStep.key) || savedSteps[index] || {}; return { key: defaultStep.key, label: String(savedStep.label ?? defaultStep.label), description: String(savedStep.description ?? defaultStep.description), items: cms.listify(savedStep.items ?? defaultStep.items), color: normalizeMethodColor(savedStep.color, defaultStep.color), art: normalizeMethodArt(savedStep.art, defaultStep.art) }; }) }; } function normalizeMethodColor(value, fallback) { const color = String(value || "").trim(); return /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(color) ? color : fallback; } function normalizeMethodArt(value, fallback = "strategy") { const art = String(value || "").trim().toLowerCase(); return ["strategy", "design", "build", "automate", "scale"].includes(art) ? art : fallback; } function normalizeAboutSettings(about) { const defaults = defaultSiteSettings.about; const source = about && typeof about === "object" ? about : {}; source.videoImage = normalizeAdminImage(source.videoImage, "about.videoImage"); source.principlesImage = normalizeAdminImage(source.principlesImage, "about.principlesImage"); source.pillarsImage = normalizeAdminImage(source.pillarsImage, "about.pillarsImage"); source.principleItems = normalizeTitleTextItems(source.principleItems, defaults.principleItems); source.pillarItems = normalizeTitleTextItems(source.pillarItems, defaults.pillarItems); source.metrics = normalizeMetricItems(source.metrics, defaults.metrics); return source; } function normalizeIndustrySettings(industries) { const defaults = defaultSiteSettings.industries; const source = industries && typeof industries === "object" ? industries : {}; source.heroImage = normalizeAdminImage(source.heroImage, "industries.heroImage"); source.stats = normalizeMetricItems(source.stats, defaults.stats); source.cards = normalizeIndustryCards(source.cards, defaults.cards); source.dispatchQueue = normalizeDispatchItems(source.dispatchQueue, defaults.dispatchQueue); source.systemItems = normalizeNumberedTextItems(source.systemItems, defaults.systemItems); return source; } function normalizePrivacyPolicySettings(policy) { const defaults = defaultSiteSettings.privacyPolicy; const source = policy && typeof policy === "object" ? policy : {}; const sourceSections = Array.isArray(source.sections) && source.sections.length ? source.sections : defaults.sections; return { kicker: String(source.kicker ?? defaults.kicker), title: String(source.title ?? defaults.title), updatedLabel: String(source.updatedLabel ?? defaults.updatedLabel), intro: String(source.intro ?? defaults.intro), sections: defaults.sections.map((defaultSection, index) => { const savedSection = sourceSections[index] || {}; return { title: String(savedSection.title ?? defaultSection.title), text: String(savedSection.text ?? defaultSection.text) }; }), contactTitle: String(source.contactTitle ?? defaults.contactTitle), contactText: String(source.contactText ?? defaults.contactText) }; } function normalizeAdminImage(image, path) { const fallback = getNestedValue(defaultSiteSettings, path) || { src: cms.FALLBACK_IMAGE, position: "center" }; const defaultImage = normalizeRawAdminImage(fallback, { src: cms.FALLBACK_IMAGE, position: "center" }); return normalizeRawAdminImage(image, defaultImage); } function normalizeRawAdminImage(image, fallback) { if (typeof image === "string") { return { src: image.trim() || fallback.src, position: fallback.position }; } return { src: String(image?.src || fallback.src).trim() || fallback.src, position: String(image?.position || fallback.position).trim() || fallback.position }; } function normalizeTitleTextItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ title: String(item?.title ?? fallback.title), text: String(item?.text ?? item?.description ?? fallback.text) })); } function normalizeMetricItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ value: String(item?.value ?? fallback.value), label: String(item?.label ?? fallback.label) })); } function normalizeIndustryCards(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ badge: String(item?.badge ?? fallback.badge), title: String(item?.title ?? fallback.title), text: String(item?.text ?? item?.description ?? fallback.text), tags: cms.listify(item?.tags ?? fallback.tags) })); } function normalizeDispatchItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ number: String(item?.number ?? fallback.number), title: String(item?.title ?? fallback.title), status: String(item?.status ?? fallback.status) })); } function normalizeNumberedTextItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ number: String(item?.number ?? fallback.number), title: String(item?.title ?? fallback.title), text: String(item?.text ?? item?.description ?? fallback.text) })); } function normalizeArrayItems(items, defaults, mapper) { const source = Array.isArray(items) && items.length ? items : defaults; return source.map((item, index) => mapper(item || {}, defaults[index] || defaults[defaults.length - 1] || {})); } function deepMerge(defaults, value) { if (Array.isArray(defaults)) { return Array.isArray(value) ? cms.clone(value) : cms.clone(defaults); } if (!defaults || typeof defaults !== "object") { return value == null ? defaults : value; } const merged = cms.clone(defaults); Object.keys(value || {}).forEach((key) => { merged[key] = deepMerge(defaults[key], value[key]); }); return merged; } function loadUploadedImage(file, item) { if (!file) return; if (!file.type.startsWith("image/")) { setStatus("Please choose an image file.", true); return; } const reader = new FileReader(); reader.onload = () => { item.image = item.image || {}; item.image.src = reader.result; updateImagePreview(reader.result); markDirty(); setStatus("Image loaded. Save changes when ready.", false); }; reader.onerror = () => setStatus("Image upload failed.", true); reader.readAsDataURL(file); } function loadUploadedSiteImage(file, path) { if (!file || !path) return; if (!file.type.startsWith("image/")) { setStatus("Please choose an image file.", true); return; } const reader = new FileReader(); reader.onload = () => { setNestedValue(state.site, `${path}.src`, reader.result); updateImagePreview(reader.result, path); markDirty(); renderEditor(); setStatus("Image loaded. Save changes when ready.", false); }; reader.onerror = () => setStatus("Image upload failed.", true); reader.readAsDataURL(file); } function loadUploadedNestedImage(file, item, path) { if (!file || !item || !path) return; if (!file.type.startsWith("image/")) { setStatus("Please choose an image file.", true); return; } const reader = new FileReader(); reader.onload = () => { if ((state.section === "services" || state.section === "technologies") && path.startsWith("insightCards.")) { prepareCurrentInsightCards(item); } setNestedValue(item, `${path}.src`, reader.result); updateImagePreview(reader.result, path); markDirty(); renderEditor(); setStatus("Image loaded. Save changes when ready.", false); }; reader.onerror = () => setStatus("Image upload failed.", true); reader.readAsDataURL(file); } function loadUploadedSiteVideo(file) { if (!file) return; if (!file.type.startsWith("video/")) { setStatus("Please choose a video file.", true); return; } if (file.size > 8 * 1024 * 1024) { setStatus("Use a video link for files over 8 MB.", true); return; } const reader = new FileReader(); reader.onload = () => { setNestedValue(state.site, "video.sourceUrl", reader.result); markDirty(); renderEditor(); setStatus("Video loaded. Save changes when ready.", false); }; reader.onerror = () => setStatus("Video upload failed.", true); reader.readAsDataURL(file); } function updateImagePreview(src, path) { const selector = path ? `[data-image-preview="${cssEscape(path)}"]` : "[data-image-preview]"; const preview = els.editorForm.querySelector(selector) || els.editorForm.querySelector("[data-image-preview]"); if (preview) preview.src = src || cms.FALLBACK_IMAGE; } function selectedAdminUser() { const index = Math.min(Math.max(state.selected.users || 0, 0), Math.max(state.users.length - 1, 0)); state.selected.users = index; return state.users[index]; } async function loadAdminUsers() { if (state.loadingUsers || state.usersLoaded) return; state.loadingUsers = true; setStatus("Loading admin users", false); try { const session = getSession(); if (!session?.token) { showLogin(); return; } const response = await fetch("api/admin-users", { headers: { Authorization: `Bearer ${session.token}` } }); if (response.status === 401) { cms.remove(cms.KEYS.session); showLogin(); return; } if (response.status === 403) { setStatus("Admin user access denied", true); return; } if (!response.ok) { throw new Error("Could not load admin users"); } const payload = await response.json(); state.users = normalizeAdminUsers(payload.users || []); state.permissionCatalog = Array.isArray(payload.permissionCatalog) && payload.permissionCatalog.length ? payload.permissionCatalog : state.permissionCatalog; state.usersLoaded = true; setStatus("Ready", false); render(); } catch (error) { setStatus("Could not load admin users", true); } finally { state.loadingUsers = false; } } function normalizeAdminUsers(users) { return (Array.isArray(users) ? users : []) .filter(Boolean) .map((user) => ({ id: String(user.id || `admin-${Date.now()}`), name: String(user.name || ""), email: String(user.email || ""), role: user.role === "master" ? "master" : "subadmin", permissions: Array.isArray(user.permissions) ? user.permissions : [], enabled: user.enabled !== false, createdAt: user.createdAt || "", updatedAt: user.updatedAt || "" })); } function addAdminUser() { state.section = "users"; if (!state.usersLoaded) { loadAdminUsers(); return; } state.usersLoaded = true; state.users.unshift({ id: `draft-${Date.now()}`, name: "New Sub Admin", email: "", role: "subadmin", permissions: ["leads"], enabled: true, password: "", isDraft: true }); state.selected.users = 0; markDirty(); render(); } async function saveAdminUser() { const user = selectedAdminUser(); if (!user) return; const session = getSession(); if (!session?.token) { showLogin(); return; } if (!user.email.trim()) { setStatus("Email is required", true); return; } if (user.isDraft && String(user.password || "").length < 6) { setStatus("New sub-admin password needs at least 6 characters", true); return; } const payload = { id: user.id, name: user.name, email: user.email, password: user.password || "", enabled: user.enabled !== false, permissions: user.role === "master" ? state.permissionCatalog.map((permission) => permission.key) : (user.permissions || []) }; setStatus("Saving admin user", false); try { const response = await fetch("api/admin-users", { method: user.isDraft ? "POST" : "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.token}` }, body: JSON.stringify(payload) }); if (response.status === 401) { cms.remove(cms.KEYS.session); showLogin(); return; } if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.error || "Save failed"); } const data = await response.json(); const savedId = data.user?.id || user.id; state.users = normalizeAdminUsers(data.users || []); state.usersLoaded = true; state.dirty = false; state.selected.users = Math.max(0, state.users.findIndex((item) => item.id === savedId)); updateStoredSessionUser(data.user); setStatus("Admin user saved", false); render(); } catch (error) { setStatus(error.message || "Admin user save failed", true); } } async function deleteAdminUser() { const user = selectedAdminUser(); if (!user || user.role === "master") return; if (user.isDraft) { state.users.splice(state.selected.users, 1); state.selected.users = 0; state.dirty = false; render(); return; } const confirmed = window.confirm(`Delete sub-admin "${user.email}"?`); if (!confirmed) return; const session = getSession(); if (!session?.token) { showLogin(); return; } setStatus("Deleting admin user", false); try { const response = await fetch("api/admin-users", { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.token}` }, body: JSON.stringify({ id: user.id }) }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.error || "Delete failed"); } const data = await response.json(); state.users = normalizeAdminUsers(data.users || []); state.selected.users = Math.max(0, state.selected.users - 1); state.dirty = false; setStatus("Sub-admin deleted", false); render(); } catch (error) { setStatus(error.message || "Delete failed", true); } } function updateStoredSessionUser(user) { if (!user?.id) return; const session = getSession(); if (session?.user?.id !== user.id) return; const updatedSession = { ...session, user, permissions: user.permissions || session.permissions }; cms.write(cms.KEYS.session, updatedSession); applySession(updatedSession); } function addWorkItem() { state.section = "work"; const title = "New Work Post"; const item = { id: `${cms.slugify(title)}-${Date.now()}`, title, category: "Website", description: "Write a concise project description for this work item.", image: { src: cms.FALLBACK_IMAGE, position: "center" }, tags: ["Website", "New"], featured: false, showOnHome: false }; state.collections.work.unshift(item); state.selected.work = 0; markDirty(); render(); } function duplicateWorkItem() { const item = selectedItem(); if (!item || state.section !== "work") return; const copy = cms.clone(item); copy.id = `${cms.slugify(copy.title)}-${Date.now()}`; copy.title = `${copy.title} Copy`; copy.featured = false; state.collections.work.splice(state.selected.work + 1, 0, copy); state.selected.work += 1; markDirty(); render(); } function deleteWorkItem() { if (state.section !== "work") return; const item = selectedItem(); if (!item) return; const confirmed = window.confirm(`Delete "${item.title}" from work posts?`); if (!confirmed) return; state.collections.work.splice(state.selected.work, 1); state.selected.work = Math.max(0, state.selected.work - 1); if (state.collections.work.length === 0) { addWorkItem(); return; } if (!state.collections.work.some((workItem) => workItem.featured)) { state.collections.work[0].featured = true; } markDirty(); render(); } async function saveCurrentSection() { if (state.section === "users") { await saveAdminUser(); return; } try { saveAllLocalCollections(); state.dirty = false; render(); } catch (error) { setStatus("Save failed. Try a smaller image.", true); return; } try { await persistContentToServer(); setStatus("Saved to site files", false); } catch (error) { setStatus(error.message?.includes("access") || error.message?.includes("expired") ? error.message : "Saved in this browser", error.message?.includes("access") || error.message?.includes("expired")); } } async function resetCurrentSection() { if (state.section === "users") return; const current = config[state.section]; const confirmed = window.confirm(`Reset ${current.title} to the default content?`); if (!confirmed) return; if (state.section === "advanced") { state.site = normalizeSiteSettings({}); saveAllLocalCollections(); state.selected.advanced = 0; state.dirty = false; render(); try { await persistContentToServer(); setStatus("Advanced settings reset", false); } catch (error) { setStatus(error.message?.includes("access") || error.message?.includes("expired") ? error.message : "Reset saved in this browser", error.message?.includes("access") || error.message?.includes("expired")); } return; } state.collections[state.section] = cms.clone(current.defaults); saveAllLocalCollections(); state.selected[state.section] = 0; state.dirty = false; render(); try { await persistContentToServer(); setStatus("Reset saved to site files", false); } catch (error) { setStatus(error.message?.includes("access") || error.message?.includes("expired") ? error.message : "Reset saved in this browser", error.message?.includes("access") || error.message?.includes("expired")); } } function saveAllLocalCollections() { Object.keys(config).forEach((section) => { if (section === "advanced" || section === "users") return; const current = config[section]; state.collections[section] = cms.saveCollection( current.key, state.collections[section], current.idField ); }); state.site = normalizeSiteSettings(state.site); cms.write("site", state.site); } async function persistContentToServer() { if (!window.fetch || !/^https?:$/i.test(window.location.protocol)) { throw new Error("Content API unavailable"); } const session = getSession(); if (!session?.token) { throw new Error("Admin session expired"); } const response = await fetch("api/content", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.token}` }, body: JSON.stringify({ services: state.collections.services, technologies: state.collections.technologies, work: state.collections.work, site: state.site }) }); if (!response.ok) { if (response.status === 401) { cms.remove(cms.KEYS.session); throw new Error("Admin session expired"); } if (response.status === 403) { throw new Error("You do not have access to save this section"); } throw new Error("Content API failed"); } return response.json(); } function exportContent() { const payload = { exportedAt: new Date().toISOString(), services: state.collections.services, technologies: state.collections.technologies, work: state.collections.work, site: state.site }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "signalhgai-content.json"; link.click(); URL.revokeObjectURL(url); setStatus("Export ready", false); } function importContent(event) { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const payload = JSON.parse(reader.result); if (Array.isArray(payload.services)) state.collections.services = payload.services; if (Array.isArray(payload.technologies)) state.collections.technologies = payload.technologies; if (Array.isArray(payload.work)) state.collections.work = payload.work; if (payload.site && typeof payload.site === "object") state.site = normalizeSiteSettings(payload.site); state.dirty = true; state.selected = { work: 0, services: 0, technologies: 0, advanced: 0 }; render(); setStatus("Imported. Save each section to publish.", false); } catch (error) { setStatus("Import failed. Check the JSON file.", true); } finally { event.target.value = ""; } }; reader.onerror = () => setStatus("Import failed.", true); reader.readAsText(file); } function markDirty() { state.dirty = true; setStatus("Unsaved changes", false); els.editorBadge.textContent = "Unsaved"; } function setStatus(message, isError) { if (!els.status) return; els.status.textContent = message; els.status.classList.toggle("is-error", Boolean(isError)); } function cssEscape(value) { if (window.CSS?.escape) return window.CSS.escape(value); return String(value || "").replace(/\\/g, "\\\\").replace(/"/g, "\\\""); } function esc(value) { return cms.escapeHtml(value); } })();
Close