From e1518f0bdfe2cd52437be3c7b14a4b41fefe77c4 Mon Sep 17 00:00:00 2001 From: Nipun Batra Date: Sat, 18 Jul 2026 10:40:27 +0530 Subject: [PATCH] Replace landing page with ecosystem guide --- .github/workflows/site-checks.yml | 20 + README.md | 37 + css/landing-page.css | 1790 ++++++++++++++++++++++++++--- favicon.svg | 4 + index.html | 671 ++++++----- js/site.js | 80 ++ scripts/check_site.py | 107 ++ 7 files changed, 2232 insertions(+), 477 deletions(-) create mode 100644 .github/workflows/site-checks.yml create mode 100644 README.md create mode 100644 favicon.svg create mode 100644 js/site.js create mode 100644 scripts/check_site.py diff --git a/.github/workflows/site-checks.yml b/.github/workflows/site-checks.yml new file mode 100644 index 0000000..9bb68db --- /dev/null +++ b/.github/workflows/site-checks.yml @@ -0,0 +1,20 @@ +name: Site checks + +on: + pull_request: + push: + branches: [master] + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Check landing page structure and links + run: python3 scripts/check_site.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..b40d457 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# NILMTK ecosystem site + +This repository publishes the canonical NILMTK ecosystem router at +. It explains which repository to use, the supported +installation paths, the container policy, citation guidance, and the route from +a model contribution to a leaderboard result. + +The site is deliberately dependency-free: semantic HTML, one stylesheet, and a +small progressive-enhancement script. Existing generated core API documentation +under `nilmtk/` remains separate from the landing page. + +## Preview + +```bash +python3 -m http.server 8000 +``` + +Then open . + +## Verify + +```bash +python3 scripts/check_site.py +``` + +The check rejects duplicate IDs, broken local assets and fragment links, +insecure public URLs, missing canonical repository or citation links, and +accidental reintroduction of the retired Bootstrap/jQuery stack. + +## Editing policy + +- Keep this page a router; detailed package APIs belong in their repositories. +- Keep one recommended Python path and one general onboarding image. +- Treat NILMbench's CPU and CUDA images as scientific runtimes, not per-model + development images. +- Update citation guidance only when the canonical repository papers change. +- Verify commands against the target repository before publishing them here. diff --git a/css/landing-page.css b/css/landing-page.css index 0f686dc..f085a15 100644 --- a/css/landing-page.css +++ b/css/landing-page.css @@ -1,165 +1,1625 @@ -body, html { - height: 100%; - width: 100%; -} - -body, h1, h2, h3, h4, h5, h6 { - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: 700; -} - -.lead { - font-size: 18px; - font-weight: 400; -} - -.intro-header { - padding-top: 50px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */ - padding-bottom: 50px; - color: #f8f8f8; - background: url(../img/intro-bg.jpg) no-repeat center center; - background-size: cover; - text-align: center; -} - -.intro-message { - position: relative; - padding-top: 20%; - padding-bottom: 20%; -} - -.intro-message > h1 { - margin: 0; - font-size: 5em; - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); -} - -.intro-divider { - width: 400px; - border-top: 1px solid #f8f8f8; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); -} - -.intro-message > h3 { - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); -} - -@media (max-width: 767px) { - .intro-message { - padding-bottom: 15%; - } - - .intro-message > h1 { - font-size: 3em; - } - - ul.intro-social-buttons > li { - display: block; - margin-bottom: 20px; - padding: 0; - } - - ul.intro-social-buttons > li:last-child { - margin-bottom: 0; - } - - .intro-divider { - width: 100%; - } -} - -.network-name { - text-transform: uppercase; - font-size: 14px; - font-weight: 400; - letter-spacing: 2px; -} - -.content-section-a { - background-color: #f8f8f8; - padding: 50px 0; -} - -.content-section-b { - border-top: 1px solid #e7e7e7; - border-bottom: 1px solid #e7e7e7; - padding: 50px 0; -} - -.section-heading { - margin-bottom: 30px; -} - -.section-heading-spacer { - border-top: 3px solid #e7e7e7; - width: 200px; - float: left; -} - -.banner { - padding: 20px 0; - color: #f8f8f8; - background: url(../img/banner-bg.jpg) no-repeat center center; - background-size: cover; - bottom:0px; - -} - -.banner h2 { - margin: 0; - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); - font-size: 3em; -} - -.banner ul { - margin-bottom: 0; -} - -.banner-social-buttons { - float: right; - margin-top: 0; -} - -@media (max-width: 1199px) { - ul.banner-social-buttons { - float: left; - margin-top: 15px; - } -} - -@media (max-width: 767px) { - .banner h2 { - margin: 0; - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); - font-size: 3em; - } - - ul.banner-social-buttons > li { - display: block; - margin-bottom: 20px; - padding: 0; - } - - ul.banner-social-buttons > li:last-child { - margin-bottom: 0; - } -} - -footer { - background-color: #f8f8f8; - padding: 50px 0; -} - -p.copyright { - margin: 15px 0 0; -} - -.img-circular{ - width: 116px; - height: 116px; - background-size: cover; - display: block; - border-radius: 100px; - -webkit-border-radius: 100px; - -moz-border-radius: 100px; -} \ No newline at end of file +:root { + --ink: #14231f; + --ink-soft: #41524d; + --paper: #f4f1e8; + --paper-deep: #e9e4d7; + --surface: #fbfaf6; + --forest: #173c33; + --forest-deep: #0e2a24; + --green: #0d6a54; + --green-bright: #41c99c; + --amber: #e49a3a; + --blue: #4c88b7; + --rose: #c86a61; + --line: rgba(20, 35, 31, 0.16); + --line-light: rgba(255, 255, 255, 0.18); + --shadow: 0 26px 70px rgba(19, 47, 39, 0.11); + --sans: "Manrope", "Helvetica Neue", Arial, sans-serif; + --serif: "Newsreader", Georgia, serif; + --mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace; + --shell: min(1180px, calc(100vw - 48px)); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 90px; +} + +body { + margin: 0; + color: var(--ink); + background: var(--paper); + font-family: var(--sans); + font-size: 16px; + line-height: 1.65; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +body::before { + position: fixed; + z-index: 100; + top: 0; + right: 0; + left: 0; + height: 3px; + background: linear-gradient(90deg, var(--green), var(--green-bright) 48%, var(--amber)); + content: ""; +} + +a { + color: inherit; + text-underline-offset: 0.18em; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button { + font: inherit; +} + +:focus-visible { + outline: 3px solid var(--amber); + outline-offset: 4px; +} + +.skip-link { + position: fixed; + z-index: 1000; + top: 12px; + left: 12px; + padding: 10px 15px; + color: white; + background: var(--forest-deep); + font-weight: 700; + transform: translateY(-160%); +} + +.skip-link:focus { + transform: translateY(0); +} + +.shell { + width: var(--shell); + margin-inline: auto; +} + +.section { + padding: 112px 0; +} + +.section-dark { + color: #f7f5ed; + background: var(--forest-deep); +} + +.section-paper { + background: var(--surface); +} + +.eyebrow { + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 20px; + color: var(--green); + font-family: var(--mono); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.12em; + line-height: 1.4; + text-transform: uppercase; +} + +.eyebrow > span { + display: block; + width: 25px; + height: 1px; + background: currentColor; +} + +.eyebrow-light { + color: var(--green-bright); +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +h1, +h2 { + font-family: var(--serif); + font-weight: 500; + letter-spacing: -0.035em; +} + +h1 { + margin-bottom: 28px; + font-size: clamp(3.5rem, 7.7vw, 6.85rem); + line-height: 0.93; +} + +h1 em { + color: var(--green); + font-style: italic; + font-weight: 500; +} + +h2 { + margin-bottom: 0; + font-size: clamp(2.65rem, 5vw, 4.7rem); + line-height: 0.98; +} + +h3 { + font-size: 1.1rem; + line-height: 1.35; +} + +.site-header { + position: sticky; + z-index: 200; + top: 0; + border-bottom: 1px solid transparent; + background: rgba(244, 241, 232, 0.88); + backdrop-filter: blur(18px); + transition: border-color 180ms ease, box-shadow 180ms ease; +} + +.site-header.is-scrolled { + border-color: var(--line); + box-shadow: 0 8px 25px rgba(20, 35, 31, 0.04); +} + +.header-inner { + display: flex; + min-height: 76px; + align-items: center; + justify-content: space-between; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + color: var(--forest); + font-size: 0.92rem; + font-weight: 800; + letter-spacing: 0.13em; + text-decoration: none; +} + +.brand-mark { + width: 43px; + overflow: visible; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.4; +} + +.primary-nav { + display: flex; + align-items: center; + gap: 31px; +} + +.primary-nav a { + color: var(--ink-soft); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.015em; + text-decoration: none; +} + +.primary-nav a:hover { + color: var(--green); +} + +.primary-nav .nav-github { + padding: 9px 15px; + border: 1px solid var(--ink); + border-radius: 999px; + color: var(--ink); +} + +.primary-nav .nav-github:hover { + color: white; + background: var(--forest); +} + +.menu-button { + display: none; +} + +.hero { + position: relative; + overflow: hidden; + padding: 94px 0 80px; +} + +.hero::before { + position: absolute; + z-index: -1; + top: -190px; + right: -240px; + width: 660px; + height: 660px; + border: 1px solid rgba(13, 106, 84, 0.13); + border-radius: 50%; + box-shadow: 0 0 0 80px rgba(13, 106, 84, 0.035), 0 0 0 160px rgba(13, 106, 84, 0.02); + content: ""; +} + +.hero-grid { + display: grid; + align-items: end; + gap: 58px; + grid-template-columns: minmax(0, 1.35fr) minmax(350px, 0.65fr); +} + +.hero-copy { + min-width: 0; +} + +.hero-lede { + max-width: 670px; + margin-bottom: 34px; + color: var(--ink-soft); + font-size: clamp(1.04rem, 1.55vw, 1.25rem); + line-height: 1.75; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 65px; +} + +.button { + display: inline-flex; + min-height: 51px; + align-items: center; + justify-content: center; + gap: 18px; + padding: 12px 20px; + border: 1px solid var(--forest); + border-radius: 4px; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.01em; + text-decoration: none; + transition: transform 160ms ease, background-color 160ms ease, color 160ms ease; +} + +.button:hover { + transform: translateY(-2px); +} + +.button-primary { + color: white; + background: var(--forest); +} + +.button-primary:hover { + background: var(--green); +} + +.button-secondary { + color: var(--forest); + background: transparent; +} + +.button-secondary:hover { + color: white; + background: var(--forest); +} + +.button-light { + border-color: var(--surface); + color: var(--forest-deep); + background: var(--surface); +} + +.hero-facts { + display: grid; + max-width: 720px; + margin: 0; + padding: 0; + border-top: 1px solid var(--line); + list-style: none; + grid-template-columns: repeat(3, 1fr); +} + +.hero-facts li { + padding: 18px 22px 0 0; +} + +.hero-facts li + li { + padding-left: 22px; + border-left: 1px solid var(--line); +} + +.hero-facts strong, +.hero-facts span { + display: block; +} + +.hero-facts strong { + margin-bottom: 3px; + color: var(--forest); + font-size: 0.82rem; +} + +.hero-facts span { + color: var(--ink-soft); + font-size: 0.68rem; +} + +.signal-panel { + position: relative; + margin-bottom: 2px; + padding: 24px 25px 20px; + border: 1px solid rgba(20, 35, 31, 0.22); + border-radius: 3px; + background: rgba(251, 250, 246, 0.58); + box-shadow: var(--shadow); +} + +.signal-panel::before { + position: absolute; + z-index: -1; + right: -16px; + bottom: -16px; + width: 68%; + height: 54%; + border: 1px solid rgba(13, 106, 84, 0.26); + border-top: 0; + border-left: 0; + content: ""; +} + +.signal-heading, +.signal-legend, +.signal-note { + display: flex; + color: var(--ink-soft); + font-family: var(--mono); + font-size: 0.61rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.signal-heading { + justify-content: space-between; + padding-bottom: 13px; + border-bottom: 1px solid var(--line); +} + +.signal { + display: block; + width: 100%; + height: auto; + margin: 12px 0 2px; + overflow: visible; +} + +.grid-lines { + fill: none; + stroke: rgba(20, 35, 31, 0.07); + stroke-width: 1; +} + +.aggregate-line, +.appliance-line { + fill: none; + stroke-linecap: square; + stroke-linejoin: miter; + vector-effect: non-scaling-stroke; +} + +.aggregate-line { + stroke: var(--forest-deep); + stroke-width: 3; +} + +.appliance-line { + stroke-width: 2.3; +} + +.line-one { stroke: var(--green); } +.line-two { stroke: var(--blue); } +.line-three { stroke: var(--amber); } + +.signal-legend { + flex-wrap: wrap; + gap: 13px; + margin-bottom: 22px; + text-transform: none; +} + +.signal-legend span { + display: flex; + align-items: center; + gap: 6px; +} + +.dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.aggregate-dot { color: var(--forest-deep); } +.one-dot { color: var(--green); } +.two-dot { color: var(--blue); } +.three-dot { color: var(--amber); } + +.signal-note { + align-items: center; + justify-content: space-between; + padding: 14px 0 0; + border-top: 1px solid var(--line); + color: var(--green); +} + +.signal-note svg { + width: 70px; + fill: none; + stroke: var(--green); + stroke-width: 1; +} + +.router { + background: var(--surface); +} + +.section-intro { + margin-bottom: 65px; +} + +.split-intro { + display: grid; + gap: 80px; + grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.65fr); +} + +.split-intro > p { + align-self: end; + margin: 0 0 4px; + color: var(--ink-soft); + font-size: 1.04rem; + line-height: 1.85; +} + +.split-intro > p a { + color: var(--green); +} + +.repo-router { + margin: 0; + padding: 0; + border-top: 1px solid var(--ink); + list-style: none; +} + +.repo-row { + position: relative; + display: grid; + min-height: 178px; + align-items: center; + gap: 28px; + padding: 30px 0; + border-bottom: 1px solid var(--line); + grid-template-columns: 55px 190px minmax(0, 1fr) 155px; + transition: padding 180ms ease, background-color 180ms ease; +} + +.repo-row::before { + position: absolute; + top: 0; + bottom: -1px; + left: -18px; + width: 4px; + background: var(--row-accent, var(--green)); + content: ""; + opacity: 0; + transform: scaleY(0.2); + transition: opacity 180ms ease, transform 180ms ease; +} + +.repo-row:hover { + padding-right: 16px; + padding-left: 16px; + background: rgba(13, 106, 84, 0.035); +} + +.repo-row:hover::before { + opacity: 1; + transform: scaleY(1); +} + +.repo-core { --row-accent: var(--green); } +.repo-metadata { --row-accent: var(--amber); } +.repo-models { --row-accent: var(--blue); } +.repo-bench { --row-accent: var(--rose); } + +.repo-number { + color: var(--ink-soft); + font-family: var(--mono); + font-size: 0.72rem; +} + +.repo-kicker { + margin-bottom: 4px; + color: var(--ink-soft); + font-family: var(--mono); + font-size: 0.63rem; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.repo-name h3 { + margin: 0; + color: var(--forest); + font-family: var(--serif); + font-size: 1.85rem; + font-weight: 600; + letter-spacing: -0.025em; +} + +.repo-purpose strong { + display: block; + margin-bottom: 8px; + color: var(--ink); + font-size: 0.93rem; +} + +.repo-purpose p { + max-width: 585px; + margin: 0; + color: var(--ink-soft); + font-size: 0.85rem; +} + +.repo-links { + display: flex; + align-items: flex-start; + flex-direction: column; + gap: 7px; +} + +.repo-links a, +.text-link, +.contribution-flow a { + color: var(--green); + font-size: 0.73rem; + font-weight: 700; + text-decoration: none; +} + +.repo-links a:hover, +.text-link:hover, +.contribution-flow a:hover { + text-decoration: underline; +} + +.architecture { + position: relative; + overflow: hidden; +} + +.architecture::after { + position: absolute; + top: -230px; + right: -220px; + width: 600px; + height: 600px; + border: 1px solid rgba(65, 201, 156, 0.15); + border-radius: 50%; + content: ""; +} + +.architecture-grid { + display: grid; + align-items: center; + gap: 75px; + grid-template-columns: minmax(280px, 0.63fr) minmax(0, 1.37fr); +} + +.architecture-copy { + position: relative; + z-index: 2; +} + +.architecture-copy h2 { + margin-bottom: 28px; +} + +.architecture-copy > p:not(.eyebrow) { + margin-bottom: 25px; + color: rgba(247, 245, 237, 0.7); + font-size: 0.94rem; +} + +.text-link-light { + color: var(--green-bright); +} + +.pipeline { + position: relative; + z-index: 2; + display: grid; + align-items: stretch; + gap: 8px; + padding-top: 65px; + grid-template-columns: 1fr 20px 1fr 20px 1fr 20px 1fr; +} + +.metadata-rail { + position: absolute; + top: 0; + right: 0; + left: 0; + display: grid; + height: 35px; + align-items: center; + border: 1px solid rgba(65, 201, 156, 0.48); + color: var(--green-bright); + font-family: var(--mono); + font-size: 0.61rem; + letter-spacing: 0.08em; + text-align: center; + text-transform: uppercase; + grid-template-columns: 1fr 1fr 1fr 1fr; +} + +.metadata-rail span { + grid-column: 1 / -1; +} + +.metadata-rail i { + position: absolute; + top: 34px; + height: 31px; + border-left: 1px dashed rgba(65, 201, 156, 0.35); +} + +.metadata-rail i:nth-of-type(1) { left: 37%; } +.metadata-rail i:nth-of-type(2) { left: 63%; } +.metadata-rail i:nth-of-type(3) { left: 88%; } + +.pipeline-step { + display: flex; + min-height: 145px; + justify-content: flex-end; + flex-direction: column; + padding: 17px; + border: 1px solid var(--line-light); + background: rgba(255, 255, 255, 0.03); +} + +.pipeline-step > span { + margin-bottom: auto; + color: var(--green-bright); + font-family: var(--mono); + font-size: 0.62rem; +} + +.pipeline-step strong, +.pipeline-step small { + display: block; +} + +.pipeline-step strong { + margin-bottom: 5px; + font-size: 0.81rem; +} + +.pipeline-step small { + color: rgba(247, 245, 237, 0.55); + font-size: 0.59rem; +} + +.pipeline-output { + border-color: rgba(65, 201, 156, 0.65); + background: rgba(65, 201, 156, 0.07); +} + +.pipeline-arrow { + display: flex; + align-items: center; + justify-content: center; + color: var(--green-bright); + font-family: var(--mono); +} + +.start { + background: var(--paper); +} + +.workbench { + display: grid; + min-height: 430px; + margin-bottom: 56px; + overflow: hidden; + border: 1px solid var(--forest); + border-radius: 4px; + box-shadow: var(--shadow); + grid-template-columns: 250px minmax(0, 1fr); +} + +.workbench-tabs { + display: flex; + flex-direction: column; + background: var(--surface); +} + +.workbench-tabs button { + position: relative; + display: flex; + min-height: 107px; + align-items: flex-start; + justify-content: center; + flex-direction: column; + padding: 20px 25px; + border: 0; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + color: var(--ink-soft); + background: transparent; + cursor: pointer; + font-size: 0.83rem; + font-weight: 700; + text-align: left; +} + +.workbench-tabs button:last-child { + border-bottom: 0; +} + +.workbench-tabs button span { + display: block; + margin-bottom: 5px; + color: var(--green); + font-family: var(--mono); + font-size: 0.56rem; + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.workbench-tabs button[aria-selected="true"] { + border-right-color: var(--forest-deep); + color: white; + background: var(--forest-deep); +} + +.workbench-tabs button[aria-selected="true"] span { + color: var(--green-bright); +} + +.workbench-tabs button[aria-selected="true"]::after { + position: absolute; + top: 50%; + right: -9px; + width: 17px; + height: 17px; + background: var(--forest-deep); + content: ""; + transform: translateY(-50%) rotate(45deg); +} + +.workbench-code { + min-width: 0; + color: #e9f1ed; + background: #102923; +} + +.code-panel { + height: 100%; +} + +.code-heading { + display: flex; + height: 59px; + align-items: center; + justify-content: space-between; + padding: 0 25px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + color: rgba(233, 241, 237, 0.55); + font-family: var(--mono); + font-size: 0.62rem; + letter-spacing: 0.04em; +} + +.code-heading button { + padding: 5px 10px; + border: 1px solid rgba(255, 255, 255, 0.23); + border-radius: 3px; + color: rgba(233, 241, 237, 0.78); + background: transparent; + cursor: pointer; + font-family: var(--mono); + font-size: 0.58rem; +} + +.code-heading button:hover { + border-color: var(--green-bright); + color: var(--green-bright); +} + +.code-panel pre { + margin: 0; + padding: 54px 44px; + overflow: auto; + font-family: var(--mono); + font-size: clamp(0.7rem, 1.2vw, 0.86rem); + line-height: 2; + white-space: pre-wrap; +} + +.code-panel code { + font-family: inherit; +} + +.code-panel .comment { + color: #75988c; +} + +.code-panel .string { + color: #76d6b4; +} + +.install-notes { + display: grid; + border-top: 1px solid var(--ink); + border-bottom: 1px solid var(--line); + grid-template-columns: repeat(3, 1fr); +} + +.install-notes article { + display: flex; + gap: 18px; + padding: 30px 28px 30px 0; +} + +.install-notes article + article { + padding-left: 28px; + border-left: 1px solid var(--line); +} + +.note-index { + color: var(--green); + font-family: var(--mono); + font-size: 0.68rem; +} + +.install-notes h3 { + margin-bottom: 8px; + font-size: 0.84rem; +} + +.install-notes p { + margin: 0; + color: var(--ink-soft); + font-size: 0.76rem; +} + +.citation { + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.citation-grid { + display: grid; + align-items: start; + gap: 80px; + grid-template-columns: minmax(270px, 0.55fr) minmax(0, 1.45fr); +} + +.citation-intro { + position: sticky; + top: 110px; +} + +.citation-intro h2 { + margin-bottom: 27px; +} + +.citation-intro > p:not(.eyebrow) { + margin-bottom: 24px; + color: var(--ink-soft); + font-size: 0.9rem; +} + +.citation-table-wrap { + min-width: 0; +} + +.citation-table { + width: 100%; + border-collapse: collapse; + font-size: 0.78rem; +} + +.citation-table th { + padding: 0 16px 15px; + border-bottom: 1px solid var(--ink); + color: var(--ink-soft); + font-family: var(--mono); + font-size: 0.57rem; + font-weight: 500; + letter-spacing: 0.08em; + text-align: left; + text-transform: uppercase; +} + +.citation-table th:first-child, +.citation-table td:first-child { + padding-left: 0; +} + +.citation-table th:last-child, +.citation-table td:last-child { + padding-right: 0; +} + +.citation-table td { + padding: 24px 16px; + border-bottom: 1px solid var(--line); + vertical-align: top; +} + +.citation-table td:first-child { + width: 38%; +} + +.citation-table td:nth-child(2) { + width: 38%; + color: var(--ink-soft); +} + +.citation-table td strong, +.citation-table td span { + display: block; +} + +.citation-table td strong { + margin-bottom: 5px; + font-size: 0.78rem; +} + +.citation-table td span { + color: var(--green); + font-family: var(--mono); + font-size: 0.57rem; + letter-spacing: 0.04em; +} + +.citation-table td a { + color: var(--green); + font-family: var(--mono); + font-size: 0.68rem; +} + +.citation-footnote { + margin: 24px 0 0; + padding: 15px 18px; + border-left: 3px solid var(--amber); + color: var(--ink-soft); + background: var(--paper); + font-size: 0.72rem; +} + +.contribute { + padding-bottom: 0; +} + +.contribution-flow { + display: grid; + margin: 0 0 90px; + padding: 0; + border-top: 1px solid var(--ink); + list-style: none; + grid-template-columns: repeat(5, 1fr); +} + +.contribution-flow li { + display: flex; + min-height: 280px; + align-items: flex-start; + flex-direction: column; + padding: 25px 20px 20px; + border-bottom: 1px solid var(--line); +} + +.contribution-flow li:first-child { + padding-left: 0; +} + +.contribution-flow li + li { + border-left: 1px solid var(--line); +} + +.contribution-flow li > span { + margin-bottom: 42px; + color: var(--green); + font-family: var(--mono); + font-size: 0.63rem; +} + +.contribution-flow h3 { + margin-bottom: 9px; + font-size: 0.9rem; +} + +.contribution-flow p { + margin-bottom: 20px; + color: var(--ink-soft); + font-size: 0.72rem; +} + +.contribution-flow a { + margin-top: auto; + font-size: 0.66rem; +} + +.contribute-cta { + display: grid; + align-items: end; + gap: 80px; + padding: 62px 68px; + color: white; + background: var(--green); + grid-template-columns: minmax(0, 1fr) auto; +} + +.contribute-cta h2 { + max-width: 700px; + margin-bottom: 20px; + font-size: clamp(2.5rem, 5vw, 4.5rem); +} + +.contribute-cta p:not(.eyebrow) { + max-width: 680px; + margin: 0; + color: rgba(255, 255, 255, 0.72); + font-size: 0.85rem; +} + +.site-footer { + padding: 82px 0 28px; + color: rgba(247, 245, 237, 0.72); + background: var(--forest-deep); +} + +.footer-grid { + display: grid; + gap: 70px; + padding-bottom: 65px; + grid-template-columns: minmax(250px, 0.75fr) minmax(0, 1.25fr); +} + +.footer-brand { + margin-bottom: 18px; + color: white; +} + +.footer-grid > div:first-child > p { + max-width: 410px; + font-size: 0.8rem; +} + +.footer-links { + display: grid; + gap: 70px; + grid-template-columns: repeat(2, 1fr); +} + +.footer-links > div { + display: flex; + flex-direction: column; + gap: 9px; +} + +.footer-links strong { + margin-bottom: 7px; + color: var(--green-bright); + font-family: var(--mono); + font-size: 0.62rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.footer-links a { + width: fit-content; + color: rgba(247, 245, 237, 0.72); + font-size: 0.76rem; + text-decoration: none; +} + +.footer-links a:hover { + color: white; +} + +.footer-bottom { + display: flex; + justify-content: space-between; + padding-top: 22px; + border-top: 1px solid var(--line-light); + color: rgba(247, 245, 237, 0.46); + font-family: var(--mono); + font-size: 0.56rem; +} + +[hidden] { + display: none !important; +} + +@media (max-width: 1050px) { + .hero-grid { + align-items: center; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); + } + + .repo-row { + grid-template-columns: 45px 165px minmax(0, 1fr) 135px; + } + + .architecture-grid { + gap: 50px; + grid-template-columns: 0.55fr 1.45fr; + } + + .pipeline { + gap: 4px; + grid-template-columns: 1fr 14px 1fr 14px 1fr 14px 1fr; + } + + .pipeline-step { + padding: 12px; + } + + .contribution-flow { + grid-template-columns: repeat(3, 1fr); + } + + .contribution-flow li:nth-child(4) { + padding-left: 0; + border-left: 0; + } +} + +@media (max-width: 820px) { + :root { + --shell: min(100% - 34px, 720px); + } + + .section { + padding: 82px 0; + } + + .menu-button { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 7px 0; + border: 0; + color: var(--forest); + background: transparent; + cursor: pointer; + font-size: 0.7rem; + font-weight: 700; + } + + .menu-icon, + .menu-icon::before, + .menu-icon::after { + display: block; + width: 19px; + height: 1px; + background: var(--forest); + content: ""; + } + + .menu-icon { + position: relative; + } + + .menu-icon::before { + position: absolute; + top: -5px; + } + + .menu-icon::after { + position: absolute; + top: 5px; + } + + .primary-nav { + position: absolute; + top: 76px; + right: 0; + left: 0; + display: none; + align-items: stretch; + flex-direction: column; + gap: 0; + padding: 12px 17px 22px; + border-bottom: 1px solid var(--line); + background: var(--paper); + box-shadow: 0 20px 30px rgba(20, 35, 31, 0.08); + } + + .primary-nav.is-open { + display: flex; + } + + .primary-nav a { + padding: 13px 0; + border-bottom: 1px solid var(--line); + } + + .primary-nav .nav-github { + width: fit-content; + margin-top: 14px; + padding: 8px 13px; + border-bottom: 1px solid var(--ink); + } + + .hero { + padding: 70px 0 66px; + } + + .hero-grid, + .split-intro, + .architecture-grid, + .citation-grid, + .contribute-cta, + .footer-grid { + grid-template-columns: 1fr; + } + + .hero-grid { + gap: 62px; + } + + .hero-actions { + margin-bottom: 45px; + } + + .signal-panel { + width: min(100%, 560px); + } + + .split-intro { + gap: 28px; + } + + .split-intro > p { + max-width: 620px; + } + + .repo-row { + gap: 14px 20px; + grid-template-columns: 40px minmax(0, 1fr); + } + + .repo-number { + grid-row: 1 / span 3; + } + + .repo-purpose, + .repo-links { + grid-column: 2; + } + + .repo-links { + flex-direction: row; + gap: 16px; + } + + .architecture-grid { + gap: 60px; + } + + .architecture-copy { + max-width: 600px; + } + + .citation-intro { + position: static; + } + + .citation-grid { + gap: 55px; + } + + .workbench { + min-height: auto; + grid-template-columns: 1fr; + } + + .workbench-tabs { + display: grid; + grid-template-columns: repeat(4, 1fr); + } + + .workbench-tabs button { + min-height: 86px; + padding: 14px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + } + + .workbench-tabs button[aria-selected="true"]::after { + top: auto; + right: auto; + bottom: -8px; + left: 50%; + transform: translateX(-50%) rotate(45deg); + } + + .code-panel pre { + min-height: 330px; + padding: 44px 30px; + } + + .install-notes { + grid-template-columns: 1fr; + } + + .install-notes article, + .install-notes article + article { + padding: 24px 0; + border-left: 0; + } + + .install-notes article + article { + border-top: 1px solid var(--line); + } + + .contribution-flow { + grid-template-columns: repeat(2, 1fr); + } + + .contribution-flow li:nth-child(3), + .contribution-flow li:nth-child(5) { + padding-left: 0; + border-left: 0; + } + + .contribution-flow li:nth-child(4) { + padding-left: 20px; + border-left: 1px solid var(--line); + } + + .contribute-cta { + gap: 35px; + padding: 50px 40px; + } +} + +@media (max-width: 560px) { + :root { + --shell: calc(100% - 28px); + } + + body { + font-size: 15px; + } + + .section { + padding: 68px 0; + } + + .header-inner { + min-height: 68px; + } + + .primary-nav { + top: 68px; + } + + .brand-mark { + width: 38px; + } + + .menu-label { + display: none; + } + + .hero { + padding-top: 54px; + } + + h1 { + font-size: clamp(3.05rem, 16vw, 4.7rem); + } + + h2 { + font-size: clamp(2.4rem, 12vw, 3.7rem); + } + + .hero-lede { + font-size: 0.98rem; + } + + .hero-actions { + align-items: stretch; + flex-direction: column; + } + + .button { + justify-content: space-between; + } + + .hero-facts { + grid-template-columns: 1fr; + } + + .hero-facts li, + .hero-facts li + li { + padding: 13px 0; + border-left: 0; + } + + .hero-facts li + li { + border-top: 1px solid var(--line); + } + + .signal-panel { + padding: 18px 16px; + } + + .signal-note svg { + width: 42px; + } + + .repo-row { + padding: 25px 0; + } + + .repo-row:hover { + padding-right: 8px; + padding-left: 8px; + } + + .repo-purpose strong { + font-size: 0.86rem; + } + + .pipeline { + padding-top: 55px; + grid-template-columns: 1fr; + } + + .metadata-rail { + height: 32px; + } + + .metadata-rail i { + display: none; + } + + .pipeline-arrow { + height: 22px; + transform: rotate(90deg); + } + + .pipeline-step { + min-height: 96px; + } + + .workbench-tabs { + overflow-x: auto; + grid-template-columns: repeat(4, minmax(120px, 1fr)); + } + + .workbench-tabs button { + min-height: 78px; + } + + .code-panel pre { + min-height: 350px; + padding: 34px 20px; + font-size: 0.66rem; + line-height: 1.85; + } + + .citation-table { + display: block; + } + + .citation-table thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + } + + .citation-table tbody, + .citation-table tr, + .citation-table td { + display: block; + width: 100% !important; + } + + .citation-table tr { + padding: 25px 0; + border-bottom: 1px solid var(--line); + } + + .citation-table td, + .citation-table td:first-child, + .citation-table td:last-child { + padding: 6px 0; + border: 0; + } + + .contribution-flow { + grid-template-columns: 1fr; + } + + .contribution-flow li, + .contribution-flow li:first-child, + .contribution-flow li:nth-child(3), + .contribution-flow li:nth-child(4), + .contribution-flow li:nth-child(5) { + min-height: 230px; + padding: 24px 0; + border-left: 0; + } + + .contribution-flow li > span { + margin-bottom: 27px; + } + + .contribute-cta { + padding: 42px 25px; + } + + .footer-grid { + gap: 42px; + } + + .footer-links { + gap: 35px; + } + + .footer-bottom { + align-items: flex-start; + flex-direction: column; + gap: 8px; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..9876dc2 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/index.html b/index.html index f6ce386..42928df 100644 --- a/index.html +++ b/index.html @@ -1,312 +1,359 @@ - - - - - - - - - - - nilmtk - - - - - - - - - - - - - - - - - -
- -
- -
-
-
-

NILMTK

-

Open source NILM toolkit

-
- -
-
-
- -
- - -
- - -
- -
- -
-
-
-
-

Convert and import - public data sets with two lines of code

-

Importers for REDD, GREEND, - WikiEnergy, AMPds, iAWE, UK-DALE and more!

- -
-
- -
-
- -
- - -
- - -
- -
- -
-
-
-
-

Data set statistics, - pre-processing and NILM metrics

-

Resample your data, filter out - erroneous readings, find gaps in your data, - find proportion of energy submetered, - calculate F1 score etc etc..

-
-
- -
-
- -
- - -
- - -
- -
- -
-
-
-
-

Compare against benchmark -
disaggregation algorithms

-

NILMTK v0.2 includes a combinatorial - optimisation NILM algorithm. FHMM and Hart's - 1985 algorithms coming soon to v0.2. More algorithms - in the pipeline!

-
-
- -
- -
- - -
-
- - - -
- -
- -
- -
- -
-
-

Publications

-

- -

    -

    -
  1. Nipun Batra, Jack Kelly, Oliver - Parson, Haimonti Dutta, William - Knottenbelt, Alex Rogers, Amarjeet - Singh, Mani Srivastava. NILMTK: An - Open Source Toolkit for Non-intrusive - Load Monitoring. In: 5th - International Conference on Future - Energy Systems (ACM e-Energy), - Cambridge, UK. 2014. - DOI:10.1145/2602044.2602051 - arXiv:1404.3878 - [slides] [bib]
  2. -
  3. Nipun Batra, Jack Kelly, Oliver - Parson, Haimonti Dutta, William - Knottenbelt, Alex Rogers, Amarjeet - Singh, Mani Srivastava. NILMTK: An - Open Source Toolkit for Non-intrusive - Load Monitoring". In: NILM - Workshop, Austin, - US. 2014 - [pdf] - [Presented as a - 30-minute IPython - demo of NILMTK v0.1 and a poster.] -
  4. -
  5. Jack Kelly, Nipun Batra, Oliver - Parson, Haimonti Dutta, William - Knottenbelt, Alex Rogers, Amarjeet - Singh, Mani Srivastava. Demo - Abstract: NILMTK v0.2: A Non-intrusive - Load Monitoring Toolkit for Large - Scale Data Sets. In the first ACM - Workshop On Embedded Systems For - Energy-Efficient Buildings, - 2014. DOI:10.1145/2674061.2675024. - arXiv:1409.5908 - [Won best demo award] - [bib] - [Presented as an - IPython demo of v0.2 - and a poster]
  6. -

    -
- -

-
- -
- -
- - -
- - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + NILMTK · Open infrastructure for energy disaggregation + + + + + + +
+
+
+
+

Open infrastructure for energy disaggregation

+

From household power
to reproducible insight.

+

NILMTK is a research ecosystem for loading real energy datasets, resolving their meaning, implementing disaggregation models, and comparing them under frozen protocols.

+ +
    +
  • Python 3.11supported research path
  • +
  • Real dataREDD · UK-DALE · REFIT
  • +
  • Open sourceApache-2.0 ecosystem
  • +
+
+ +
+
+ aggregate mains + power / time +
+ + Aggregate power separated into appliance signals + A dark aggregate power trace branches into three coloured appliance-level traces. + + + + + + + + + +
+ mains + fridge + washer + kettle +
+
+ One measured channel + + Appliance estimates +
+
+
+
+ +
+
+
+
+

Choose by job

+

Four repositories.
Four distinct responsibilities.

+
+

Start with the question you are trying to answer. The packages work together, but each has one canonical job. This separation keeps data semantics, model code, and scientific claims reviewable.

+
+ +
    +
  1. +
    01
    +
    +

    Data layer

    +

    NILMTK

    +
    +
    + Load, convert, inspect, and score energy data. +

    Use core for dataset converters, buildings and meters, preprocessing, metrics, and classical baselines.

    +
    + +
  2. + +
  3. +
    03
    +
    +

    Model layer

    +

    nilmtk-contrib

    +
    +
    + Use or contribute a disaggregation model. +

    Use contrib for maintained classical, TensorFlow, and PyTorch implementations behind shared NILMTK contracts.

    +
    + +
  4. +
  5. +
    04
    +
    +

    Evidence layer

    +

    NILMbench

    +
    +
    + Reproduce protocols and compare real runs. +

    Use the benchmark for T1/T2/T3 tasks, provenance, CPU smoke tests, CUDA runs, immutable results, and the living leaderboard.

    +
    + +
  6. +
+
+
+ +
+
+
+

The system

+

One path from files to claims.

+

NILM Metadata supplies shared meaning across the entire path. Core turns physical files into consistent meters and windows. Contrib supplies models. NILMbench freezes the comparison and publishes only provenance-complete results.

+ Explore the organization +
+ +
+
+ +
+
+
+
+

Start clean

+

A supported route,
not another dated tutorial.

+
+

Use Python 3.11 and uv. Pick the smallest installation that matches your job. The model install brings core and metadata with it.

+
+ +
+
+ + + + +
+
+
+
PyTorch model environment
+
# Python 3.11 · CPU-safe install
+uv venv --python 3.11
+source .venv/bin/activate
+UV_TORCH_BACKEND=cpu uv pip install \
+  "nilmtk-contrib[torch] @ git+https://github.com/nilmtk/nilmtk-contrib.git"
+python -c "from nilmtk_contrib.torch import Seq2PointTorch; print('ready')"
+
+ + + +
+
+ +
+
+ A +

Bring licensed data

Datasets are not baked into packages or images. Download them from their official custodians, follow their licenses, and mount local NILMTK HDF5 conversions read-only.

+
+
+ B +

Keep one general image

The contrib Dockerfile defines the common starting environment. Model contributions do not create a new container tag or image family.

+
+
+ C +

Separate benchmark runtimes

NILMbench owns pinned CPU-smoke and CUDA-benchmark runtimes. Those images certify results; they are not per-algorithm development images.

+
+
+
+
+ +
+
+
+

Cite precisely

+

Credit the layer
your work relies on.

+

There is no single citation that describes every part of the current ecosystem. Cite the infrastructure paper for each layer you use, plus the original model and dataset papers.

+ Copy BibTeX from the repositories +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
If your work uses…CiteDOI
Dataset conversion, meters, preprocessing, or metricsNILMTK coreNILMTK: An Open Source Toolkit for Non-intrusive Load Monitoring10.1145/2602044.2602051
Schema, taxonomy, synonyms, or dataset semanticsNILM MetadataTowards Reproducible Research in Energy Disaggregation10.1109/COMPSACW.2014.97
The shared model suite or rapid experimentation APInilmtk-contribTowards Reproducible State-of-the-Art Energy Disaggregation10.1145/3360322.3360844
T1/T2/T3 protocols, runner, or leaderboard resultsNILMBench2026 · BuildSys '26NILMBench2026: A Benchmark for Energy Disaggregation10.1145/3744256.3812587
+

Also cite: every evaluated model's original paper and every dataset used. A leaderboard row is evidence, not a substitute for source attribution.

+
+
+
+ +
+
+
+
+

Extend the ecosystem

+

One model contribution.
One auditable route to rank.

+
+

Algorithm code and benchmark evidence move at different cadences. A model can merge once its implementation contract is sound. It enters the leaderboard only after the pinned benchmark matrix passes on real data.

+
+ +
    +
  1. 01

    Implement

    Add the model to nilmtk-contrib using shared validation, windowing, checkpoint, and logging utilities.

    Model repository ↗
  2. +
  3. 02

    Break it on purpose

    Test malformed inputs, short chunks, lazy imports, determinism, serialization, CPU smoke, and the targeted CUDA path.

    Model tests ↗
  4. +
  5. 03

    Register

    Add only the adapter and search space to NILMbench. Dataset and task logic stays in the benchmark, never in a model notebook.

    Benchmark repository ↗
  6. +
  7. 04

    Run real data

    Freeze source revisions, image digest, data identity, hardware, seeds, and protocol. Candidate results remain separate.

    Run protocol ↗
  8. +
  9. 05

    Publish evidence

    Review immutable result bundles; CI regenerates the leaderboard and rejects hand-edited or incomparable ranks.

    Living leaderboard ↗
  10. +
+ +
+
+

New here?

+

Start with a reproducible failure.

+

Open an issue with the repository, exact command, operating system, Python version, dataset identity, and the smallest trace that reproduces the problem.

+
+ Choose a repository +
+
+
+
+ + + + diff --git a/js/site.js b/js/site.js new file mode 100644 index 0000000..a130831 --- /dev/null +++ b/js/site.js @@ -0,0 +1,80 @@ +const header = document.querySelector("[data-header]"); +const menuButton = document.querySelector("[data-menu-button]"); +const menu = document.querySelector("[data-menu]"); +const tabs = Array.from(document.querySelectorAll("[data-tab]")); + +function updateHeader() { + header?.classList.toggle("is-scrolled", window.scrollY > 8); +} + +function closeMenu() { + menu?.classList.remove("is-open"); + menuButton?.setAttribute("aria-expanded", "false"); + menuButton?.setAttribute("aria-label", "Open navigation menu"); +} + +menuButton?.addEventListener("click", () => { + const isOpen = menu?.classList.toggle("is-open") ?? false; + menuButton.setAttribute("aria-expanded", String(isOpen)); + menuButton.setAttribute( + "aria-label", + isOpen ? "Close navigation menu" : "Open navigation menu", + ); +}); + +menu?.querySelectorAll("a").forEach((link) => link.addEventListener("click", closeMenu)); + +document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeMenu(); + menuButton?.focus(); + } +}); + +window.addEventListener("scroll", updateHeader, { passive: true }); +window.addEventListener("resize", () => { + if (window.innerWidth > 820) closeMenu(); +}); +updateHeader(); + +function selectTab(tab) { + tabs.forEach((candidate) => { + const selected = candidate === tab; + candidate.setAttribute("aria-selected", String(selected)); + candidate.tabIndex = selected ? 0 : -1; + const panel = document.getElementById(candidate.dataset.tab); + if (panel) panel.hidden = !selected; + }); +} + +tabs.forEach((tab, index) => { + tab.addEventListener("click", () => selectTab(tab)); + tab.addEventListener("keydown", (event) => { + if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; + event.preventDefault(); + let nextIndex = index; + if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length; + if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length; + if (event.key === "Home") nextIndex = 0; + if (event.key === "End") nextIndex = tabs.length - 1; + selectTab(tabs[nextIndex]); + tabs[nextIndex].focus(); + }); +}); + +document.querySelectorAll("[data-copy]").forEach((button) => { + button.addEventListener("click", async () => { + const source = document.getElementById(button.dataset.copy); + if (!source) return; + const original = button.textContent; + try { + await navigator.clipboard.writeText(source.textContent.trim()); + button.textContent = "Copied"; + } catch { + button.textContent = "Select code"; + } + window.setTimeout(() => { + button.textContent = original; + }, 1600); + }); +}); diff --git a/scripts/check_site.py b/scripts/check_site.py new file mode 100644 index 0000000..42e42e5 --- /dev/null +++ b/scripts/check_site.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Dependency-free structural checks for the NILMTK ecosystem landing page.""" + +from __future__ import annotations + +from collections import Counter +from html.parser import HTMLParser +from pathlib import Path +import sys +from urllib.parse import unquote, urlsplit + + +ROOT = Path(__file__).resolve().parents[1] +INDEX = ROOT / "index.html" + +REQUIRED_URLS = { + "https://github.com/nilmtk/nilmtk", + "https://github.com/nilmtk/nilm_metadata", + "https://github.com/nilmtk/nilmtk-contrib", + "https://github.com/nilmtk/nilmbench", + "https://nilmtk.github.io/nilmbench/", + "https://doi.org/10.1145/2602044.2602051", + "https://doi.org/10.1109/COMPSACW.2014.97", + "https://doi.org/10.1145/3360322.3360844", + "https://doi.org/10.1145/3744256.3812587", +} + + +class SiteParser(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.ids: list[str] = [] + self.links: list[tuple[str, str]] = [] + + def handle_starttag( + self, tag: str, attrs: list[tuple[str, str | None]] + ) -> None: + values = dict(attrs) + if identifier := values.get("id"): + self.ids.append(identifier) + for attribute in ("href", "src"): + if target := values.get(attribute): + self.links.append((attribute, target)) + + +def local_target(target: str) -> Path | None: + parsed = urlsplit(target) + if parsed.scheme or parsed.netloc or target.startswith(("mailto:", "tel:")): + return None + if not parsed.path: + return None + relative = unquote(parsed.path).lstrip("/") + return ROOT / relative + + +def main() -> int: + source = INDEX.read_text(encoding="utf-8") + parser = SiteParser() + parser.feed(source) + errors: list[str] = [] + + duplicates = sorted(name for name, count in Counter(parser.ids).items() if count > 1) + if duplicates: + errors.append(f"duplicate HTML ids: {', '.join(duplicates)}") + + known_ids = set(parser.ids) + for attribute, target in parser.links: + parsed = urlsplit(target) + if parsed.path in ("", INDEX.name) and parsed.fragment: + if parsed.fragment not in known_ids: + errors.append(f"broken fragment link: {target}") + path = local_target(target) + if path is not None and not path.exists(): + errors.append(f"missing local {attribute}: {target}") + + if "http://" in source: + errors.append("public page contains an insecure http:// URL") + + missing_urls = sorted(url for url in REQUIRED_URLS if url not in source) + if missing_urls: + errors.append("missing canonical links: " + ", ".join(missing_urls)) + + retired_assets = ("jquery", "bootstrap.css", "bootstrap.js") + for asset in retired_assets: + if asset in source.lower(): + errors.append(f"retired landing-page dependency referenced: {asset}") + + required_assets = (ROOT / "css/landing-page.css", ROOT / "js/site.js", ROOT / "favicon.svg") + for asset in required_assets: + if not asset.is_file() or asset.stat().st_size == 0: + errors.append(f"required asset is absent or empty: {asset.relative_to(ROOT)}") + + if errors: + print("Site checks failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + print( + f"Site checks passed: {len(parser.ids)} ids, " + f"{len(parser.links)} links/assets, {len(REQUIRED_URLS)} canonical URLs." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())