diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f4f5dc8..fd78f2f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -9,6 +9,9 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +permissions: + contents: read + jobs: validate: runs-on: ubuntu-latest @@ -16,6 +19,12 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Build generated site + run: npm run build:site + + - name: Run launch-safety checks + run: npm run check + - name: Check index.html exists and is non-empty run: test -s index.html diff --git a/README.md b/README.md index 4fd00e8..4a807b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# StackScout +# Stack Scout -`tools-hub` builds **StackScout**, the public-facing tools destination for curated builder tools, services, APIs, MCPs, and CLIs. +`stackscout` builds **Stack Scout**, the public-facing tools destination for curated builder tools, services, APIs, MCPs, and CLIs. This repo remains the GitHub Pages implementation base, but the visible product is no longer a simple internal "Tools Hub" brochure. The private operational console stays separate in `W:\Repos\_local\surfaces\tools-hub-local`. @@ -13,7 +13,7 @@ This repo remains the GitHub Pages implementation base, but the visible product ## Shared source layer -StackScout uses a shared source layer inside this repo: +Stack Scout uses a shared source layer inside this repo: - `content/stackscout/site-source.json` - `content/stackscout/tools-source.json` @@ -50,6 +50,18 @@ This regenerates: npm run check ``` +`npm run check` also runs the no-publish launch-safety gate: + +```bash +npm run verify:launch +``` + +That gate scans generated public output for local Windows paths and private surface markers, confirms the public file set exists, checks `.gitignore` still excludes local notes and env files, and verifies the `service-worker.js` cache name is not older than the generated issue date. + +GitHub Pages does not support custom response headers such as a Netlify `_headers` file. Keep browser hardening inside static HTML, conservative client code, and dependency-free scripts unless the site moves to a host that can set CSP/HSTS-style headers. + +Before a public refresh, bump `CACHE_NAME` in `service-worker.js` when generated public content advances. The launch-safety gate fails if the cache date is older than the visible issue date. + ## Refresh ```bash @@ -63,13 +75,13 @@ For unattended Windows refreshes without visible terminal focus theft, use the l ## Site structure - `Home` -- `Catalog` +- `Top tools` - `Tool Detail` - `Categories` -- `Updates` +- `Wire` - `Radar` - `Collections` -- `Method` +- `Sources & method` ## Launch surface highlights @@ -77,13 +89,13 @@ For unattended Windows refreshes without visible terminal focus theft, use the l - shareable catalog filters via URL query state - public dossier pages for every tracked tool - source-linked updates and visible freshness dates -- clearly labelled `StackScout Lab` subset for in-house tools +- clearly labelled `Stack Scout Lab` subset for in-house tools - installable static PWA shell for repeat visits ## Notes -- StackScout is curated ecosystem first. -- Our own tools are a clearly labelled `StackScout Lab` subset, not the whole point of the site. +- Stack Scout is curated ecosystem first. +- Our own tools are a clearly labelled `Stack Scout Lab` subset, not the whole point of the site. - Public verdicts use editorial badges, not fake numeric scoring. - Update items should prefer official release notes, changelogs, docs, blogs, and first-party repositories. - The catalog now keeps filter state in the URL so filtered views can be shared directly. diff --git a/app.js b/app.js index fefe7a6..72b8d41 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,51 @@ document.body.classList.add('has-motion') +const MOOD_CLASSES = ['mood-graphite', 'mood-midnight', 'mood-obsidian', 'mood-slate', 'mood-carbon'] + +function initMoodSwitcher() { + const buttons = Array.from(document.querySelectorAll('[data-mood]')) + if (!buttons.length) { + return + } + + const storedMood = localStorage.getItem('stackscout:mood') + const initialMood = buttons.some((button) => button.dataset.mood === storedMood) ? storedMood : 'graphite' + + function applyMood(mood) { + document.documentElement.classList.remove(...MOOD_CLASSES) + document.documentElement.classList.add(`mood-${mood}`) + buttons.forEach((button) => { + const isActive = button.dataset.mood === mood + button.classList.toggle('is-active', isActive) + button.setAttribute('aria-pressed', String(isActive)) + }) + localStorage.setItem('stackscout:mood', mood) + } + + buttons.forEach((button) => { + button.addEventListener('click', () => applyMood(button.dataset.mood)) + }) + + applyMood(initialMood) +} + +function initUtcClock() { + const clock = document.getElementById('utcClock') + if (!clock) { + return + } + + function renderClock() { + const now = new Date() + const hours = String(now.getUTCHours()).padStart(2, '0') + const minutes = String(now.getUTCMinutes()).padStart(2, '0') + clock.textContent = `Live - ${hours}:${minutes} UTC` + } + + renderClock() + window.setInterval(renderClock, 30_000) +} + function initReveal() { const revealItems = Array.from(document.querySelectorAll('[data-reveal]')) if (!revealItems.length) { @@ -24,7 +70,7 @@ function initReveal() { }) }, { - threshold: 0.16, + threshold: 0.02, rootMargin: '0px 0px -8% 0px', }, ) @@ -173,3 +219,5 @@ function initCatalogFilters() { initReveal() initCatalogFilters() +initMoodSwitcher() +initUtcClock() diff --git a/catalog/index.html b/catalog/index.html index 2d9c3e8..5c4a8e6 100644 --- a/catalog/index.html +++ b/catalog/index.html @@ -1,47 +1,39 @@ - +
-