diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4b3d428..af4c566 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -25,7 +25,7 @@ This is a **static marketing website** for RocketPy (rocketpy.org) - an open-sou ### Image Assets - Background images are referenced in CSS using optimized assets (e.g., `url("../images/hero-background.png")`) -- Section illustrations use descriptive filenames (e.g., `hero-logo-illustration.png`, `community-visual.png`) +- Section illustrations are scalable SVGs with descriptive filenames (e.g., `hero-trajectory.svg`, `community-network.svg`) - Favicon and logo files are in `images/` directory ## Development Workflow @@ -36,6 +36,15 @@ npm run format # Runs Prettier on all HTML, CSS, JS files ``` Only dev dependency is Prettier (v3.3.3). Always format before committing. +### Local Preview +```bash +make start # serve the source files at http://127.0.0.1:8000 (override with PORT=) +make status # check whether the dev server is running +make stop # stop the dev server +make build # build the minified site into dist/ +``` +The `make` targets wrap `scripts/devserver.py`, a cross-platform (Windows/macOS/Linux) manager for a detached `python -m http.server`. Run `make help` to list all targets. + ### Deployment - **Automatic deployment** via GitHub Actions (`.github/workflows/static.yml`) - Triggers on push to `master` branch diff --git a/.gitignore b/.gitignore index 22e8474..3334be8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ node_modules/ # Build output dist/ +# Local dev server +.devserver.pid + # OS files .DS_Store Thumbs.db diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..62397b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +# RocketPy site - local dev helpers. +# Run `make help` to list the available targets. + +PORT ?= 8000 +HOST ?= 127.0.0.1 +export PORT HOST + +.PHONY: help start stop restart status build serve-dist format + +help: + @echo Targets: + @echo make start - start the local dev server on the source files + @echo make stop - stop the dev server + @echo make restart - restart the dev server + @echo make status - show whether the server is running + @echo make build - build the minified site into dist/ + @echo make serve-dist - build then preview the production dist/ output + @echo make format - run prettier on html/css/js + @echo Override the port with e.g. make start PORT=3000 + +start: + @python scripts/devserver.py start + +stop: + @python scripts/devserver.py stop + +restart: + @python scripts/devserver.py restart + +status: + @python scripts/devserver.py status + +build: + @npm run build + +serve-dist: build + @python scripts/devserver.py stop + @python -m http.server $(PORT) --directory dist --bind $(HOST) + +format: + @npm run format diff --git a/about.html b/about.html index b022149..eefe8ba 100644 --- a/about.html +++ b/about.html @@ -3,15 +3,15 @@ - RocketPy Team - About + RocketPy Team - About Us - + @@ -22,10 +22,10 @@ - + - - - - - - -
-

Redirecting to RocketPy homepage...

-

- The About page is being rebuilt. If you are not redirected, continue to - rocketpy.org. -

-
+ + function renderFeatured(gridId, tier) { + const grid = document.getElementById(gridId); + if (!grid) return; + TEAM.featured + .filter((p) => p.tier === tier) + .forEach((p) => { + const card = document.createElement("div"); + card.className = `featured-card tier-${tier}`; + card.innerHTML = ` + + + + + + `; + grid.appendChild(card); + }); + } + + function renderContributors() { + const grid = document.getElementById("contribGrid"); + if (!grid) return; + TEAM.gallery.forEach((p) => { + const cell = document.createElement(p.handle ? "a" : "div"); + cell.className = "contrib-cell"; + if (p.handle) { + cell.href = `https://github.com/${p.handle}`; + cell.target = "_blank"; + cell.rel = "noopener noreferrer"; + } + cell.title = `${p.name}${p.commits ? " — " + p.commits + " commits" : ""}`; + cell.innerHTML = ` + ${avatarMarkup(p, "contrib-avatar")} + ${p.name} + `; + grid.appendChild(cell); + }); + } + + function renderStats() { + const people = TEAM.featured.length + TEAM.gallery.length; + const el = document.getElementById("statPeople"); + if (el) el.textContent = people + "+"; + } + + renderFeatured("foundersGrid", "founder"); + renderFeatured("coreGrid", "core"); + renderContributors(); + renderStats(); + diff --git a/css/about.css b/css/about.css new file mode 100644 index 0000000..9a67266 --- /dev/null +++ b/css/about.css @@ -0,0 +1,615 @@ +/* ========================================================================== + ROCKETPY ABOUT / TEAM PAGE STYLES (about.css) + Timeless "story + people" layout: hero, story timeline, founders, + maintainers and a full hall-of-contributors gallery. + ========================================================================== */ + +:root { + --accent: #f0ab50; + --accent-soft: #ffd38f; + --accent-founder: #f0ab50; + --accent-maintainer: #2dd4bf; + --panel: rgba(33, 35, 50, 0.45); + --panel-strong: rgba(33, 35, 50, 0.65); + --hairline: rgba(255, 255, 255, 0.08); + --hairline-strong: rgba(255, 255, 255, 0.18); + --ink-soft: rgba(255, 255, 255, 0.75); + --ink-faint: rgba(255, 255, 255, 0.55); +} + +/* ------------------------------- HERO ---------------------------------- */ +.about-hero { + text-align: center; + padding: clamp(2.5rem, 6vw, 4.5rem) 1rem clamp(1.5rem, 4vw, 2.5rem); + position: relative; +} + +.about-eyebrow { + font-family: "Ruda", sans-serif; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 14px; +} + +.about-title { + font-family: "Rubik", sans-serif; + font-size: clamp(2rem, 5vw, 3.4rem); + font-weight: 700; + letter-spacing: 0.02em; + margin-bottom: 16px; + background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.about-subtitle { + color: var(--ink-soft); + font-family: "Open Sans", sans-serif; + font-size: clamp(1rem, 2.5vw, 1.18rem); + line-height: 1.65; + max-width: 720px; + margin: 0 auto; +} + +/* Hero stat row */ +.stat-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: clamp(1.2rem, 5vw, 3.5rem); + margin-top: clamp(1.8rem, 4vw, 2.6rem); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 90px; +} + +.stat-num { + font-family: "Rubik", sans-serif; + font-size: clamp(1.5rem, 4vw, 2.1rem); + font-weight: 700; + color: #ffffff; + line-height: 1.1; +} + +.stat-label { + font-family: "Ruda", sans-serif; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ink-faint); + margin-top: 6px; +} + +/* ---------------------------- SECTIONS --------------------------------- */ +.about-section { + max-width: 1100px; + margin: clamp(2.5rem, 7vw, 5rem) auto 0; + padding: 0 1rem; +} + +.section-title { + font-family: "Rubik", sans-serif; + font-size: clamp(1.5rem, 3.5vw, 2.2rem); + font-weight: 700; + color: #ffffff; + text-align: center; + letter-spacing: 0.01em; + position: relative; + margin-bottom: 8px; +} + +.section-title::after { + content: ""; + display: block; + width: 54px; + height: 3px; + margin: 14px auto 0; + border-radius: 3px; + background: linear-gradient( + 130deg, + var(--accent) 0%, + var(--accent-soft) 100% + ); +} + +.section-lede { + font-family: "Open Sans", sans-serif; + font-size: clamp(0.95rem, 2vw, 1.08rem); + color: var(--ink-soft); + line-height: 1.6; + text-align: center; + max-width: 680px; + margin: 18px auto 2.5rem; +} + +/* ----------------------------- STORY ----------------------------------- */ +.story-prose { + max-width: 760px; + margin: 0 auto 3rem; +} + +.story-prose p { + font-family: "Open Sans", sans-serif; + font-size: clamp(1rem, 2vw, 1.1rem); + line-height: 1.8; + color: var(--ink-soft); + margin-bottom: 1.25rem; +} + +.story-prose strong { + color: #ffffff; + font-weight: 700; +} + +.story-prose a, +.section-lede a, +.contrib-note a { + color: var(--accent-soft); + text-decoration: none; + border-bottom: 1px solid rgba(255, 211, 143, 0.35); + transition: border-color 0.2s ease; +} + +.story-prose a:hover, +.section-lede a:hover, +.contrib-note a:hover { + border-color: var(--accent-soft); +} + +/* Timeline */ +.timeline { + list-style: none; + padding: 0; + margin: 0 auto; + max-width: 780px; + position: relative; +} + +.timeline::before { + content: ""; + position: absolute; + left: 70px; + top: 8px; + bottom: 8px; + width: 2px; + background: linear-gradient( + to bottom, + rgba(240, 171, 80, 0.6), + rgba(240, 171, 80, 0.05) + ); +} + +.tl-item { + position: relative; + display: grid; + grid-template-columns: 70px 1fr; + gap: 24px; + padding: 0 0 26px 0; +} + +.tl-item:last-child { + padding-bottom: 0; +} + +.tl-year { + font-family: "Rubik", sans-serif; + font-size: 15px; + font-weight: 700; + color: var(--accent); + text-align: right; + padding-top: 1px; + white-space: nowrap; +} + +.tl-item::before { + content: ""; + position: absolute; + left: 65px; + top: 4px; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 4px rgba(240, 171, 80, 0.18); +} + +.tl-body h3 { + font-family: "Rubik", sans-serif; + font-size: 1.05rem; + font-weight: 700; + color: #ffffff; + margin: 0 0 4px; +} + +.tl-body p { + font-family: "Open Sans", sans-serif; + font-size: 0.95rem; + line-height: 1.6; + color: var(--ink-soft); + margin: 0; +} + +/* --------------------- FEATURED (founders / maintainers) --------------- */ +.featured-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 24px; + justify-content: center; +} + +.featured-card { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 34px 26px; + border-radius: 20px; + background: var(--panel); + border: 1px solid var(--hairline); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: relative; + overflow: hidden; + transition: + transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.35s ease, + border-color 0.35s ease; +} + +.featured-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + opacity: 0.85; +} + +.featured-card.tier-founder::before { + background: linear-gradient(90deg, var(--accent) 0%, var(--accent-soft) 100%); +} + +.featured-card.tier-core::before { + background: linear-gradient(90deg, #14b8a6 0%, #99f6e4 100%); +} + +.featured-card:hover { + transform: translateY(-8px); + background: var(--panel-strong); + border-color: var(--hairline-strong); + box-shadow: 0 20px 44px rgba(0, 0, 0, 0.42); +} + +.featured-avatar-wrap { + width: 104px; + height: 104px; + margin-bottom: 20px; + border-radius: 50%; + padding: 3px; + background: linear-gradient( + 135deg, + var(--accent) 0%, + var(--accent-soft) 100% + ); +} + +.tier-core .featured-avatar-wrap { + background: linear-gradient(135deg, #14b8a6 0%, #99f6e4 100%); +} + +.featured-avatar { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + display: block; + background: #0b1020; +} + +.featured-name { + font-family: "Rubik", sans-serif; + font-size: 1.25rem; + font-weight: 700; + color: #ffffff; + margin: 0 0 6px; +} + +.featured-role { + font-family: "Ruda", sans-serif; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 14px; + color: var(--accent); +} + +.tier-core .featured-role { + color: var(--accent-maintainer); +} + +.featured-bio { + font-family: "Open Sans", sans-serif; + font-size: 0.92rem; + line-height: 1.6; + color: var(--ink-soft); + margin: 0 0 18px; + flex-grow: 1; +} + +.featured-socials { + display: flex; + gap: 12px; + margin-top: auto; +} + +.gh-link { + color: var(--ink-faint); + font-size: 20px; + padding: 4px; + transition: + color 0.2s ease, + transform 0.2s ease; +} + +.gh-link:hover { + color: #ffffff; + transform: translateY(-2px); +} + +/* Avatar fallback (initials) */ +.avatar-fallback { + display: flex; + align-items: center; + justify-content: center; + font-family: "Rubik", sans-serif; + font-weight: 700; + color: #ffffff; + background: radial-gradient( + circle at 30% 30%, + #3b4a6b 0%, + #1e293b 60%, + #0f172a 100% + ); +} + +.featured-avatar.avatar-fallback { + font-size: 32px; +} + +/* ----------------------- HALL OF CONTRIBUTORS -------------------------- */ +.contrib-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 14px; +} + +.contrib-cell { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 10px; + padding: 18px 10px; + border-radius: 14px; + background: rgba(15, 23, 42, 0.35); + border: 1px solid var(--hairline); + text-decoration: none; + transition: + transform 0.25s ease, + background 0.25s ease, + border-color 0.25s ease; +} + +a.contrib-cell:hover { + transform: translateY(-4px); + background: rgba(33, 35, 50, 0.6); + border-color: rgba(240, 171, 80, 0.35); +} + +.contrib-avatar { + width: 56px; + height: 56px; + border-radius: 50%; + object-fit: cover; + display: block; + background: #0b1020; + border: 1px solid var(--hairline-strong); +} + +.contrib-avatar.avatar-fallback { + font-size: 18px; +} + +.contrib-name { + font-family: "Open Sans", sans-serif; + font-size: 12.5px; + font-weight: 600; + line-height: 1.3; + color: var(--ink-soft); + word-break: break-word; +} + +a.contrib-cell:hover .contrib-name { + color: #ffffff; +} + +.contrib-note { + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + color: var(--ink-faint); + text-align: center; + max-width: 620px; + margin: 2rem auto 0; + line-height: 1.6; +} + +/* -------------------------- GLOBAL / INSTITUTIONS ---------------------- */ +.inst-countries { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 12px; + font-family: "Ruda", sans-serif; + font-size: clamp(0.95rem, 2vw, 1.1rem); + color: var(--ink-soft); + margin: -1rem auto 2.5rem; +} + +.inst-country-stat strong { + color: var(--accent); + font-size: 1.15em; +} + +.inst-dot { + color: var(--ink-faint); +} + +.inst-grid { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); + gap: 14px; +} + +.inst-card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 18px 20px; + border-radius: 14px; + background: rgba(15, 23, 42, 0.35); + border: 1px solid var(--hairline); + border-left: 3px solid var(--accent); + transition: + transform 0.25s ease, + background 0.25s ease; +} + +.inst-card:hover { + transform: translateY(-3px); + background: rgba(33, 35, 50, 0.55); +} + +.inst-name { + font-family: "Rubik", sans-serif; + font-size: 1rem; + font-weight: 700; + color: #ffffff; +} + +.inst-place { + font-family: "Open Sans", sans-serif; + font-size: 0.85rem; + color: var(--ink-faint); +} + +/* ------------------------------- CTA ----------------------------------- */ +.about-cta { + text-align: center; + border-radius: 24px; + padding: clamp(2rem, 5vw, 3.5rem) 1.5rem; + margin-bottom: clamp(2.5rem, 7vw, 5rem); +} + +.cta-buttons { + display: flex; + flex-wrap: wrap; + gap: 16px; + justify-content: center; +} + +.cta-btn { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: "Ruda", sans-serif; + font-size: 15px; + font-weight: 700; + padding: 13px 26px; + border-radius: 999px; + text-decoration: none; + transition: + transform 0.2s ease, + box-shadow 0.2s ease, + background 0.2s ease; +} + +.cta-primary { + background: linear-gradient( + 130deg, + var(--accent) 0%, + var(--accent-soft) 100% + ); + color: #030b28; + box-shadow: 0 6px 20px rgba(240, 171, 80, 0.35); +} + +.cta-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 26px rgba(240, 171, 80, 0.5); +} + +.cta-secondary { + background: rgba(255, 255, 255, 0.06); + color: #ffffff; + border: 1px solid var(--hairline-strong); +} + +.cta-secondary:hover { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.12); +} + +/* ---------------------------- RESPONSIVE ------------------------------- */ +@media (max-width: 600px) { + .timeline::before { + left: 54px; + } + + .tl-item { + grid-template-columns: 54px 1fr; + gap: 16px; + } + + .tl-item::before { + left: 49px; + } + + .tl-year { + font-size: 13px; + } + + .contrib-grid { + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); + gap: 10px; + } + + .contrib-avatar { + width: 48px; + height: 48px; + } +} + +@media (prefers-reduced-motion: reduce) { + .featured-card, + .contrib-cell, + .cta-btn, + .gh-link { + transition: none; + } +} diff --git a/css/footer.css b/css/footer.css index a582176..3d2b43a 100644 --- a/css/footer.css +++ b/css/footer.css @@ -79,13 +79,13 @@ .footer-distributed .footer-center i { background-color: #33383b; color: #ffffff; - font-size: 25px; - width: 100%; + font-size: 18px; + width: 38px; height: 38px; border-radius: 50%; text-align: center; - line-height: 42px; - margin: 10px 15px; + line-height: 38px; + margin: 10px 12px 10px 0; vertical-align: middle; } diff --git a/css/main.css b/css/main.css index 81050a4..c7e9d91 100644 --- a/css/main.css +++ b/css/main.css @@ -273,18 +273,23 @@ body::before { color: var(--accent-strong); } -.hero-logo-graphic, +.hero-graphic, .environment-graphic, -.predictions-graphic { +.predictions-graphic, +.community-graphic { display: block; margin: 0 auto; - max-width: 330px; width: 100%; } -.hero-plot-graphic, +.hero-graphic { + max-width: 440px; +} + +.environment-graphic, +.predictions-graphic, .community-graphic { - display: block; + max-width: 340px; } .section-graphic { @@ -294,18 +299,6 @@ body::before { width: 100%; } -.hero-plot-graphic { - height: 190px; - margin-top: var(--space-content); - max-width: 320px; - width: 100%; -} - -.community-graphic { - max-width: 360px; - width: 100%; -} - .reveal-section { opacity: 0; transform: translateY(26px); @@ -384,7 +377,7 @@ body::before { .split-layout { grid-template-columns: 1fr; - row-gap: 16px; + row-gap: clamp(1.25rem, 5vw, 2rem); } .section-title, @@ -399,9 +392,16 @@ body::before { margin-right: auto; } - .hero-plot-graphic { - margin-left: auto; - margin-right: auto; + /* On stacked layouts the illustration reads as the section's supporting + visual, so keep it modest and let the text lead. */ + .hero-graphic { + max-width: 380px; + } + + .environment-graphic, + .predictions-graphic, + .community-graphic { + max-width: 300px; } } @@ -423,4 +423,14 @@ body::before { .content-section { padding: 20px; } + + .hero-graphic { + max-width: 300px; + } + + .environment-graphic, + .predictions-graphic, + .community-graphic { + max-width: 250px; + } } diff --git a/images/community-network.svg b/images/community-network.svg new file mode 100644 index 0000000..e48508f --- /dev/null +++ b/images/community-network.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/community-visual.png b/images/community-visual.png deleted file mode 100644 index 95633cd..0000000 Binary files a/images/community-visual.png and /dev/null differ diff --git a/images/community-visual.webp b/images/community-visual.webp deleted file mode 100644 index d08c2e0..0000000 Binary files a/images/community-visual.webp and /dev/null differ diff --git a/images/environment-atmosphere.svg b/images/environment-atmosphere.svg new file mode 100644 index 0000000..60f4de8 --- /dev/null +++ b/images/environment-atmosphere.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/environment-visual.png b/images/environment-visual.png deleted file mode 100644 index 2977063..0000000 Binary files a/images/environment-visual.png and /dev/null differ diff --git a/images/environment-visual.webp b/images/environment-visual.webp deleted file mode 100644 index f186236..0000000 Binary files a/images/environment-visual.webp and /dev/null differ diff --git a/images/hero-logo-illustration.png b/images/hero-logo-illustration.png deleted file mode 100644 index 31a6549..0000000 Binary files a/images/hero-logo-illustration.png and /dev/null differ diff --git a/images/hero-logo-illustration.webp b/images/hero-logo-illustration.webp deleted file mode 100644 index 11a8229..0000000 Binary files a/images/hero-logo-illustration.webp and /dev/null differ diff --git a/images/hero-trajectory.svg b/images/hero-trajectory.svg new file mode 100644 index 0000000..9c6c5c1 --- /dev/null +++ b/images/hero-trajectory.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/monte-carlo-dispersion.svg b/images/monte-carlo-dispersion.svg new file mode 100644 index 0000000..b3de491 --- /dev/null +++ b/images/monte-carlo-dispersion.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/monte-carlo-visual.png b/images/monte-carlo-visual.png deleted file mode 100644 index 299e19e..0000000 Binary files a/images/monte-carlo-visual.png and /dev/null differ diff --git a/images/monte-carlo-visual.webp b/images/monte-carlo-visual.webp deleted file mode 100644 index bba0616..0000000 Binary files a/images/monte-carlo-visual.webp and /dev/null differ diff --git a/images/simulation-plot.png b/images/simulation-plot.png deleted file mode 100644 index 7ef67b2..0000000 Binary files a/images/simulation-plot.png and /dev/null differ diff --git a/images/simulation-plot.webp b/images/simulation-plot.webp deleted file mode 100644 index 05a2025..0000000 Binary files a/images/simulation-plot.webp and /dev/null differ diff --git a/index.html b/index.html index 7d3c71f..d625f14 100644 --- a/index.html +++ b/index.html @@ -147,51 +147,31 @@

The sky is not the limit!

-

+

RocketPy is an innovative rocket trajectory simulator that includes features never seen before in the state-of-the-art rocket trajectory simulators.

SIMULATE WITH ROCKETPY - - - - -
- - - RocketPy logo illustration - +
+ A rocket ascending along a simulated flight trajectory
@@ -223,21 +203,15 @@

- - - Atmospheric data visualization - + Atmospheric layers and weather sounding data above a planet horizon
@@ -268,21 +242,15 @@

- - - Monte Carlo impact confidence interval plot - + Monte Carlo impact dispersion with a confidence ellipse around the target
@@ -309,21 +277,15 @@

- - - RocketPy community network illustration - + A connected network of RocketPy contributors around a central hub
diff --git a/package.json b/package.json index c74395e..6d63d09 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "scripts": { "format": "prettier --write '**/*.{html,css,js}'", "build": "npm run build:css && npm run build:html", - "build:css": "npm run minify:css:main && npm run minify:css:footer", + "build:css": "npm run minify:css:main && npm run minify:css:footer && npm run minify:css:about", "build:html": "npm run minify:html:index && npm run minify:html:about", "minify:css:main": "lightningcss --minify --bundle css/main.css -o dist/css/main.css", "minify:css:footer": "lightningcss --minify --bundle css/footer.css -o dist/css/footer.css", + "minify:css:about": "lightningcss --minify --bundle css/about.css -o dist/css/about.css", "minify:html:index": "html-minifier --collapse-whitespace --remove-comments index.html -o dist/index.html", "minify:html:about": "html-minifier --collapse-whitespace --remove-comments about.html -o dist/about.html", "prebuild": "node -e \"const fs = require('fs'); fs.rmSync('dist', { recursive: true, force: true }); ['dist', 'dist/css', 'dist/images', 'dist/about-images'].forEach(dir => fs.mkdirSync(dir, { recursive: true }));\"", diff --git a/scripts/devserver.py b/scripts/devserver.py new file mode 100644 index 0000000..19fecc1 --- /dev/null +++ b/scripts/devserver.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Local dev-server manager for the RocketPy static site. + +Cross-platform (Windows / macOS / Linux) so the Makefile targets behave the +same regardless of whether `make` runs recipes through cmd.exe or a POSIX +shell. Starts `python -m http.server` detached, tracks it via a PID file, and +can stop/restart it. + +Usage: + python scripts/devserver.py {start|stop|restart|status} + +Environment: + PORT port to serve on (default: 8000) + HOST address to bind (default: 127.0.0.1) +""" + +import os +import signal +import socket +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PID_FILE = ROOT / ".devserver.pid" +HOST = os.environ.get("HOST", "127.0.0.1") +PORT = int(os.environ.get("PORT", "8000")) +IS_WIN = os.name == "nt" + + +def port_open(): + """True if something is already accepting connections on HOST:PORT.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(0.4) + return sock.connect_ex((HOST, PORT)) == 0 + + +def read_pid(): + try: + return int(PID_FILE.read_text().strip()) + except (FileNotFoundError, ValueError): + return None + + +def _urls(): + base = f"http://{HOST}:{PORT}" + return base, f"{base}/index.html", f"{base}/about.html" + + +def start(): + if port_open(): + base, index, about = _urls() + print(f"Server already running at {base}") + print(f" Landing: {index}") + print(f" About: {about}") + return + + args = [sys.executable, "-m", "http.server", str(PORT), "--bind", HOST] + kwargs = { + "cwd": str(ROOT), + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL, + } + if IS_WIN: + # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP -> survives make exiting. + kwargs["creationflags"] = 0x00000008 | 0x00000200 + else: + kwargs["start_new_session"] = True + + proc = subprocess.Popen(args, **kwargs) + PID_FILE.write_text(str(proc.pid)) + + for _ in range(30): + if port_open(): + break + time.sleep(0.1) + else: + print("Server did not come up in time; check for errors.", file=sys.stderr) + sys.exit(1) + + base, index, about = _urls() + print(f"Server started at {base} (pid {proc.pid})") + print(f" Landing: {index}") + print(f" About: {about}") + + +def stop(): + pid = read_pid() + killed = False + if pid is not None: + if IS_WIN: + result = subprocess.run( + ["taskkill", "/PID", str(pid), "/F", "/T"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + killed = result.returncode == 0 + else: + try: + os.kill(pid, signal.SIGTERM) + killed = True + except OSError: + killed = False + + if PID_FILE.exists(): + PID_FILE.unlink() + + # Wait for the socket to be released so a following start() is reliable. + for _ in range(20): + if not port_open(): + break + time.sleep(0.1) + + if killed: + print(f"Server stopped (pid {pid}).") + else: + print("No running server found (nothing to stop).") + + +def restart(): + stop() + start() + + +def status(): + base, index, about = _urls() + state = "RUNNING" if port_open() else "stopped" + print(f"Server is {state} at {base}") + if state == "RUNNING": + print(f" Landing: {index}") + print(f" About: {about}") + + +COMMANDS = {"start": start, "stop": stop, "restart": restart, "status": status} + + +def main(): + command = sys.argv[1] if len(sys.argv) > 1 else "status" + action = COMMANDS.get(command) + if action is None: + print(f"Unknown command: {command}", file=sys.stderr) + print(f"Usage: python {Path(__file__).name} {{{'|'.join(COMMANDS)}}}", file=sys.stderr) + sys.exit(1) + action() + + +if __name__ == "__main__": + main()