Say how fast it is, and name what carries it - #127
Conversation
The site's CSS was one 200-line file appended to Bootstrap, and the order it was written in hid a bug. Our theme values landed *after* the Bootstrap import, where Bootstrap's own !default declarations had already won, so $primary stayed at Bootstrap's #0d6efd while :root separately declared --bs-blue: #4c60d8. The page carried two blues: buttons and links in one, headings and the navbar in the other. Nobody had written the wrong colour anywhere -- the file just could not express the intent in that order. So the entry point is now only an ordering statement: fonts, Bootstrap's functions, our variables, Bootstrap, then our own partials. --bs-primary and --bs-btn-bg both compile to #4c60d8 now. $blue is set alongside $primary because --bs-blue is what the pre-relaunch rules colour headings with, and leaving it at the default would have reproduced the same split from the other side. The partials that arrive here are the ones that style pages which exist today. Components and page styles belonging to the new pages come with those pages, rather than sitting in the tree as dead CSS in the meantime. pages/_legacy.scss holds the rules that belong to the pre-relaunch pages, kept verbatim and kept together so the cutover can delete a file instead of picking rules apart. One of them is the CSS that hides the Russian integrators from non-Russian visitors on /introduction: the replacement is a Ruby helper, but until that page goes this rule is the only thing enforcing it, and dropping it early would show that logo wall to everyone. The other legacy rule worth naming is the h2 title style. _base.scss gives article headings the new treatment; the legacy selector is longer and therefore wins, and it applies exactly to pages rendered inside the .container wrapper -- which is every page that is not full-bleed. The two rules split along the same line the layout's fullwidth switch will. FONTS. The stylesheet opened with three @import url() lines: two to fonts.googleapis.com and one to cdn.jsdelivr.net. Every visitor announced themselves to two third parties before the page could paint, and first paint sat behind DNS lookups we do not control. This is the same argument that moved the legacy logo off a maintainer's personal CDN in d60729a. IBM Plex and bootstrap-icons are now served from public/fonts, subset to latin, latin-ext and cyrillic; Chinese falls back to the system stack, because IBM Plex has no CJK glyphs and the families that do are megabytes. The files are committed, since public/ bypasses Sprockets and a generated file would have to exist before assets:precompile in the image and before CI renders a page, for no benefit. tools/copy-fonts.mjs regenerates them from the @fontsource packages and fails loudly if a file the stylesheet asks for is missing. JAVASCRIPT. One window.onload handler becomes modules under app/javascript/src. That handler assigned window.onload rather than adding a listener, so any second assignment would have silently replaced all of it, and it waited on every image -- on the Open Wall, the whole gallery -- before anything became interactive. It is DOMContentLoaded now. The zoom initialiser called `new bootstrap.Modal(document.getElementById(...))` unconditionally. Most pages have no zoomable image and so no #modalZoom, the constructor threw on null, and because every initialiser shared the one handler, the throw took external links, timestamps and confirmations down with it on those pages. It returns early now. @hotwired/turbo-rails and @hotwired/stimulus are removed: neither was ever imported. The layout's data-turbo-track goes with them, having been inert. The lockfile is regenerated with Yarn 4 -- it must never be rewritten by a v1 yarn, which produces a format `yarn install --immutable` in the Dockerfile cannot read. Bootstrap is imported per component rather than wholesale. 189.4 KB to 181.8 KB unminified, which is a small win; the point is the import list, which now states what the markup actually depends on. Audited against the views first: dropdown, collapse, offcanvas, carousel and Modal are used, and nothing uses tooltip, popover, tab, alert or scrollspy. Verified: 231 runs, 873 assertions, 0 failures. Diffing the compiled selector set against master's leaves exactly one difference -- label.required:after, which is now spelled ::after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The <head> carried a title and nothing else. A link to openipc.org shared in Telegram, in a forum post, or shown in a search result therefore appeared as a bare URL with no summary and no image -- for a project whose reach is almost entirely word of mouth between people posting links to each other. Added: a description, an OG/Twitter card set, a canonical, and a theme colour. Three of those have a detail worth keeping: The canonical carries the path and not the query string. ?locale=ru is a rendering of the same page rather than a different one, and pointing all three translations at one canonical is what stops them competing with each other for the same content. og:image is absolute, because crawlers do not resolve a relative one, and it lives in public/ rather than the asset pipeline. OG images are fetched once and cached by URL by every consumer that has ever seen the link, so a fingerprint in the name would invalidate those caches for no gain. The two font faces every page renders body text in are preloaded. crossorigin is required even though they are same-origin -- fonts are fetched in CORS mode, and a preload without it is simply fetched a second time. The navbar and footer move into layouts/ partials. Nothing about them changes: same links, same order, same locale switcher, and the footer's icons still resolve through asset_path, which is not cosmetic -- production runs with config.assets.compile = false, so a literal /assets/... path is a 404, and 2ec9baa fixed exactly these lines. Extracting them now means the relaunch can rewrite the navigation in one file a reviewer can read, instead of inside a diff that also touches <head>. <main> gains a content_for(:fullwidth) switch for pages that lay out their own full-bleed sections. Flash messages keep the container in both branches, or they would run edge to edge. Pages that set nothing keep the wrapper they have always had, and a test holds that. The tests are the ones that would have caught each way a <head> goes wrong: a missing or duplicated description, a description that came back as "translation missing", a canonical that kept the query string, a relative og:image, an og-default.png that is not there, and a preload naming a font file the stylesheet does not generate. 238 runs, 903 assertions, 0 failures. i18n-tasks reports no missing keys and 102 unused, unchanged -- the new key being used is what keeps that number flat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
Seven pages -- home, get-started, low-latency, ecosystem, business, community, donate -- plus the shared partials they are built from. They answer on their own URLs from this commit, but the root route and the navigation still serve the pre-relaunch structure, so nothing a visitor sees changes. The cutover is a separate change, and this one can go to dev.openipc.org and be looked at first. The copy is written in all three locales the site now serves. English and Russian are as drafted; the Chinese was written for this commit and has not been read by a native speaker -- it should be, before it is linked to. i18n-tasks reports 347 keys in each of the three files and no missing translations. Not everything from the draft survived contact with the current tree, and the rendering tests are what found each one. /get-started hardcoded four WebUI screenshots by filename -- webui/preview.jpg and three others. The gallery was reshot at twice the resolution in 0f42514 and every one of those files stopped existing; the page raised on render. It now takes them from WebuiGallery, the same manifest /web-interface reads and the same one tools/webui-gallery photographs from, so the two cannot disagree about what exists. Tile and full-resolution copy are passed as a pair, which is what data-zoom in src/zoom.js is for. Every link in the new copy that pointed at wiki.openipc.org now points at github.com/OpenIPC/wiki. That host is retired; 2842ffa and 73c0961 removed the last references to it and this copy would have put four of them back. The partner wall moves out of the markup into PagesHelper, transcribed from /introduction as it stands today rather than from the June draft, which was ten entries out of date. Entries that page has commented out are commented out here too, with their URLs, so nothing appears or disappears silently when /introduction goes. REVIEW POINT: the Russian integrator block is currently commented out on /introduction, so no visitor sees any of it. Rendering it for :ru brings it back. That is what the relaunch plan asks for and it is gated to :ru as the maintainers asked, but it is a content decision -- say so if it should stay hidden. Worth knowing: the CSS that was supposed to enforce that gate, `html:not([lang="ru"]) article.introduction .col.logo.ru`, has never matched anything. No logo on that page carries the `ru` class it selects on. The gate being in Ruby now is what makes it testable, and there is a test. /open-wall is uncommented. It was commented out in ed0e025, a bulk tidy-up, while five places that redirect to it were left in place -- twice in snapshots_controller, once in admin/snapshots_controller, and the breadcrumb on three views. All five fell through the catch-all and answered a 302 to the homepage. This exposes nothing new: `resources :snapshots` has served the same gallery at /snapshots throughout. Snapshot.latest_per_camera lifts the correlated subquery out of SnapshotsController#index, because the homepage mosaic wants the same list and two copies of that query is one too many. pages.donate belonged to the band rendered under every page. It is now the /donate page's, and the band's two keys moved to pages.donate_band -- otherwise lazy lookup in the partial and the new page's title would have fought over the same key, and the band would have lost its please_support string. 272 runs, 1223 assertions, 0 failures. Each page is rendered in each of the three locales and checked for "translation missing"; every internal link on them is followed and required not to land on the catch-all; the homepage is rendered against an empty database, which is what a fresh checkout has. rubocop is unchanged at 53 offences across the files this touches, and the two new files add none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The root becomes pages#home, the navigation becomes Get Started | Hardware | Low Latency | Ecosystem | For Business | Community, and the single copyright line becomes a four-column footer. Everything that has been building up over the last four changes is now what a visitor sees. The pre-relaunch URLs redirect rather than disappear. They are in search results, in forum posts and in the wiki, and none of that is ours to edit: 301 for the ones whose destination is settled, so what is indexed transfers instead of competing, and 302 for /about alone, because it is meant to become a page of its own and a 301 is cached by browsers indefinitely -- it would outlive the decision. There is a test per redirect, and a test that every target resolves, because a redirect to a path that has itself gone still answers 301 here and lands the visitor on the homepage. TWO ROUTES THAT ANSWERED 500. Building the new menu meant looking at every URL it would link, and two of them had never worked: /tools/bandwidth-calculator routed to pages#bandwidth_calculator. There is no such action and no such template. Every request raised AbstractController::ActionNotFound, which in production is a 500. /tools/timelaps-interval-calculator routed to pages#timelaps_interval_ calculator, which likewise does not exist -- the template is there but named with hyphens, so nothing could render it, and it has no translations either. Both routes are removed, so an inbound link now reaches the homepage rather than an error page. Neither page is linked from the new navigation, and the unfinished timelapse template is left in the tree: finishing it is a decision for whoever started it, not something to settle with a routing change. The upload API keeps its own test. Cameras POST to /snapshots and the gallery moved to /open-wall; the API did not move at all, and nothing in the relaunch may shadow it. That test found a third thing. Snapshot read Rails.application.credentials.mac.blacklisted directly, so without config/master.key -- which is what a fresh checkout and the test environment have -- credentials.mac is nil and every upload raised NoMethodError rather than simply having nothing to blacklist. Production has the key, so it never showed there; it made the API impossible to exercise anywhere else. It digs now. The pages that were replaced are deleted, along with their translations and the navigation entries that pointed at them: i18n-tasks unused goes from 102 to 72, below where it started, rather than up. pages/_legacy.scss loses two of its three rules with the pages they styled -- including the CSS territory gate that never matched anything. Two white stripes showed up in the rendered page where one dark band met the next: the CTA band and the footer both carried a top margin on top of their own padding. Both dropped. 289 runs, 1276 assertions, 0 failures. i18n-tasks reports no missing translations across en, ru and zh. rubocop is 734 offences over 114 files against 742 over 111 on master. Every page was rendered in a browser in all three languages before this was committed, which is how the footer stylesheet turned out never to have been imported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
An audit of the June working tree against what the five preceding changes actually landed found 20 files unaccounted for. Seventeen were deliberate and are explained in those changes -- the concern upstream rewrote better, the locale files for languages no longer served, a Yarn 1 lockfile, a schema dump. Three were not deliberate. They were missed. MERCHANDISE. The page advertises a single T-shirt three times: the same image, the same .shirt_1 keys and the same link, pasted into three cards. It has read as a catalogue of one product in triplicate on the live site for as long as the file has existed. One card now, and a test that counts them. THE OPEN WALL SAYS WHAT IT IS. The wall is in the navigation and the footer since the cutover, so it now gets visitors who have never seen it and cannot tell from a grid of stills whether these images were collected or volunteered. A line under the heading says they were volunteered, and that they are deleted after two days -- which is PurgeImagesJob::RETENTION, checked rather than assumed. THE SURVIVING PAGES HAD NO TESTS. /green_life, /merchandise, /stages-of-firmware-development, /utilities and the partition and timer calculators are served by the app and were rendered by no test at all. They are the pages nobody thinks about, and they render through the layout, navbar and footer that the relaunch rewrote -- so a mistake in any of those shows up there first, or not at all. Ten of them are rendered now, checked for their own title and for translation-missing, and held to the layout wrapper, which pins the other side of the content_for(:fullwidth) switch against pages that depend on it. 303 runs, 1390 assertions, 0 failures. i18n-tasks reports no missing translations and 72 unused, unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The existing link test walks whatever the page renders and requires each href to resolve. That cannot notice a link which stopped being rendered at all -- a pillar card losing its href, or a whole section dropped in a refactor -- because a page with fewer links still passes. This is the one assertion from the June test file that the two replacements did not carry between them. 304 runs, 1404 assertions, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
The terminal block on /get-started ended at "curl -L -o /tmp/ipctool https://github.com/OpenI". The rest of the line was behind a horizontal scrollbar, which most systems do not draw until you touch the element, so there was nothing on the page to suggest the command continued at all. This is the command the page asks a reader to paste into a root shell on their camera. Being able to read it to the end before running it is the reason for showing the command rather than linking a script. The block was in a one-third column, and the ipctool release URL is wider than that column. It moves under the three steps and takes the full width of the article, where the two commands are two lines at desktop width. The stylesheet also wraps instead of scrolling now, which is what handles the narrow viewports where no layout change can help: at 390px the command runs to four lines, all of them visible. The copy button copies textContent rather than innerText. innerText is the rendered text, and with the block now soft-wrapping a long URL there is no guarantee an engine will not fold a visual break into the string. textContent is the markup's own text: the command, with only the newlines that were written. A test holds the command to the page whole -- the tempting fix for a clipped line is to shorten what is displayed, and that would leave the page telling people to run something other than what works. The URL was checked while fixing this and answers 200. Measured at 1280, 820 and 390 px: nothing clipped at any of them. 305 runs, 1411 assertions, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hvzXBErEjEGMyRme2K3hi
PR Summary by QodoReplace latency claims with audited real-world measurements
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1.
|
The latency table on /low-latency was unchanged 2022 announcement copy. Users have pasted the exact sentence back into the chats twice asking how to reach it. It described gk7205v200/hi3516-era cameras decoded on laptops, while the paragraphs around it now describe Waybeam, PixelPilot and Aviateur -- none of which existed when those numbers were measured. An audit of 923,581 messages across the OpenIPC and wfb-ng groups found the figures optimistic by 40-160 ms at the exact configurations they named, and at the same time understating the floor by half: the hero said "from about 60 ms" when the best-measured result on this stack is 26 ms. The fix is one number and four bars, not a datasheet. Resolution turns out to be nearly free -- the same camera at 720p45 moves 120 ms on the monitor alone -- so the bars compare receive paths, which is what actually decides what a reader will get. Each spans the lowest and highest figures people report for that path; collapsing a path to a single number is how the old table came to promise something nobody could reach. The audit supports a great deal more than this: a definition of glass-to-glass, a monitor sweep, the 66-99 ms an HDMI capture adapter can cost before a camera is even attached, and the 40 ms between two receivers listed here as equivalents. That is wiki material. This is a landing page, so it gets the claim and the evidence for it, and the rest stays where detail belongs. Two things deliberately left out. The figures are not linked to their sources: the reports live at t.me/c/... URLs that resolve only for members of those private groups, and the reporters are real people who have not been asked whether they want their names on the marketing site. And the claim that these were "measured with our own open hardware latency meter" is gone rather than restated -- the meter exists, but its design, method and runs are not published, and these numbers are not its output.
260722b to
dada766
Compare
The radio-link card credited wfb-ng. devourer is OpenIPC's own replacement for it -- a drop-in in the same chain, Waybeam on the camera, devourer over the air, PixelPilot or Aviateur on the ground -- so that is what the card and the TX -> ... -> RX line under it now name. What makes it worth naming is where it runs. devourer drives the Realtek adapter from userspace over libusb, with no kernel module in the path, which is what lets it set rate, power and channel per frame rather than per association. Adaptive link, frequency hopping and narrowband channels all fall out of that, and none of them are reachable through a vendor .ko. The credits still thank both projects, which is the one line the change does not take away from wfb-ng. The provenance line under the latency bars still reads "the OpenIPC and wfb-ng chats" and stays that way: it says where the reported figures were collected, and they were collected there.
c740862 to
98f5b6f
Compare
/ecosystem described devourer as "an open foundation for SDR-like receivers built on cheap Wi-Fi hardware" and badged it R&D. Both undersell it by about the same margin the 60 ms claim did. It is not a foundation for receivers; it is the link, the piece between waybeam_venc on the camera and PixelPilot or Aviateur on the ground. And R&D means "we have the SDK and are starting to study the platform", which is the wrong end of the lifecycle for something PixelPilot ships on Android today across five Realtek hardware backends. The rest of that section is already DONE, and so is this. Reordered to chain order at the same time -- camera, air, ground -- so the three read as the path a frame actually takes.
|
/review |
|
Code review by qodo was updated up to the latest commit 6a1a189 |
The unit was stated once, under the axis, which works for anyone who takes the block in at a glance. A screen reader arrives at the figures one at a time and heard "Dedicated ground station", "26 to 67", with nothing saying what of until the end of the block -- and by then the numbers are behind it. Each value now carries the unit in a visually-hidden span, so every row reads complete: "Dedicated ground station, 26-67 ms glass-to-glass". The axis and its unit label are decoration for those same numbers, so both are now aria-hidden rather than read out a second time. Nothing moves on screen; the sighted reader still sees the bare range and the unit once, under the axis.
devourer earned two sentences on the middle card while TX still had the one line it was written with, so the row read as one card with a paragraph in it flanked by two captions -- four lines against two. All three now run to the same length in every locale: 4 lines in English, 5 in Russian, 3 in Chinese, measured rather than eyeballed. The middle card gave up a clause it did not need; TX gained the one fact it was missing, which is that the frame rate chosen there sets most of the budget the rest of the page is about.
Eight findings across #123-#127, fixed here at the tip rather than on each branch, because the stack merges in order and this is where they all end up. - The homepage called upcase on every snapshot's soc and sensor. Both are nullable and the upload endpoint permits either to be absent, so one such upload took the whole page down. The caption now drops what is missing, and disappears when both are. - The "runs on silicon by" strip listed every Vendor, including the ones that only make image sensors. Vendor.soc_vendors exists for exactly this and is now what the homepage asks for. - latest_per_camera called every row sharing a camera's maximum created_at the latest, because the anti-join only compared timestamps. Two uploads a second apart spent two of the homepage's five tiles on one camera. Ties now break on id. - The legacy redirects used redirect('/path'), which drops the query string, and locale rides in the query string: /introduction?locale=ru landed on the homepage in the browser's language. They keep it now. - The /snapshots regression test accepted 500 as a valid request error, so a controller regression that returned a server error passed as though the camera API still worked. An empty upload is a 415. - The Open Wall intro said every image was "uploaded voluntarily by a camera running OpenIPC firmware" -- the endpoint is unauthenticated and verifies neither -- and promised deletion "after two days", which a once-nightly purge cannot meet. Both now say what is actually true. - The ipctool command is step 1's, but rendered after all three steps, so below the lg breakpoint the reader met steps 2 and 3 before the command they needed for step 1. It sits after step 1 now and moves to the end of the row only on desktop, where it wants the full width anyway. Each fix has a test, and each test was checked against the unfixed code first: reverting the four that a request can reach fails all four. Not fixed: the finding that the self-hosted fonts bypass the asset pipeline. They are served from public/, which is what public/ is for, and all 19 of them answer 200 with font/woff2 on dev.
…mbers # Conflicts: # app/assets/stylesheets/application.bootstrap.scss # app/controllers/pages_controller.rb # app/helpers/pages_helper.rb # app/models/snapshot.rb # app/views/pages/ecosystem.html.erb # app/views/pages/get_started.html.erb # app/views/pages/home.html.erb # app/views/pages/low_latency.html.erb # config/locales/en.yml # config/locales/pages.en.yml # config/locales/pages.ru.yml # config/locales/pages.zh.yml # config/locales/ru.yml # config/locales/zh.yml # config/routes.rb # test/controllers/redirects_test.rb # test/controllers/relaunch_pages_test.rb
The comment still asserted that two days is what PurgeImagesJob::RETENTION enforces, which is the claim the copy above it stopped making: the constant is a cutoff for a nightly sweep, not a deadline.
Three commits, all on the low-latency story: the numbers, the project that
carries the link, and the entry that describes that project.
1. Say how fast it is, in four bars
The latency table on
/low-latencywas unchanged 2022 announcement copy.Users have pasted the exact sentence back into the chats twice asking how to
reach it. Measured against a 2026 audit of the OpenIPC and wfb-ng archives
(923,581 messages):
And the hero undersold us by half: "from about 60 ms" states a floor, and
the best-measured figure on this stack is 26 ms.
Resolution was also the wrong axis — it is nearly free. The same camera at
720p45 moves 120 ms on the monitor alone.
carried the same sentence.
path — dedicated ground station, goggles with a built-in VRX, phone or
tablet, laptop or desktop — each spanning the lowest and highest figures
users report. One measure on one shared scale, so one hue rather than
four: the reader is comparing magnitude, not telling series apart.
It is a landing page, so it gets the claim and the evidence for it. The
audit supports a great deal more — a definition of glass-to-glass, the full
monitor sweep, the 66–99 ms an HDMI capture adapter costs before a camera is
attached — and that is wiki material.
2. Name our own link on the card that describes the radio link
The radio-link card credited wfb-ng. devourer is our own in-place
replacement for it, in the same chain:
waybeam_vencon the camera,devourer over the air, PixelPilot or Aviateur on the ground. So that is what
the card and the
TX → … → RXline under it now name.What makes it worth naming is where it runs: devourer drives the Realtek
adapter from userspace over libusb, with no kernel module in the path, which
is what lets it set rate, power and channel per frame rather than per
association. Adaptive link, frequency hopping and narrowband channels all
fall out of that.
The credits still thank both — "The radio link exists thanks to wfb-ng and
devourer" — and the provenance line under the bars still reads "the OpenIPC
and wfb-ng chats", because that is where the figures were collected.
3. Say what devourer is on the page that lists it
/ecosystemdescribed it as "an open foundation for SDR-like receiversbuilt on cheap Wi-Fi hardware" and badged it R&D. It is not a foundation
for receivers, it is the link; and R&D means "we have the SDK and are
starting to study the platform", which is the wrong end of the lifecycle for
something PixelPilot ships on Android across five Realtek hardware backends.
Now
The link itself: a userspace driver for Realtek Wi-Fi adapters, setting rate, power, and channel packet by packet.at DONE, and moved intochain order so the cards read camera → air → ground.
Two things deliberately not done
No source links on the figures. Every one is a user report living at a
t.me/c/...URL, which resolves only for members of those private groupsand would 404 for a visitor. The reporters are also real people who have not
been asked whether they want their names on the marketing site. A test
asserts no
t.me/c/link ever appears.The meter claim is removed, not restated. The meter exists, but its
design, method and raw runs are not published, and these figures are not its
output.
Verification
bin/rails testi18n-tasks missingi18n-tasks unusedrubocopstill cannot start on this branch —.rubocop.ymlrequiresrubocop-performanceand the Gemfile does not list it. That is thepre-existing defect #120 fixes.
The Chinese copy is mine and has not been read by a native speaker. The
DONE badge on devourer is my judgement from its README and its use in
PixelPilot — say the word if it should be WIP or MVP.