fix(docs): the retired favicon, a broken home-page card, and a 1.61:1 print border — each with the gate that missed it (#96)
Closes the loose ends left over from the phase-5 work, plus the two
workstreams
that were still outstanding (W13 per-page OG cards, W14 icon/manifest
hygiene).
Three of the five things fixed here were **shipped defects nobody could
have
caught**, because in each case the pipeline had no check that joined the
two
halves involved. Each is now closed by a gate as well as by the fix.
### `favicon.ico` was the retired shield
Every icon is rendered from `logo-light.svg` — except the `.ico`, which
was last
written by the *July* logo redesign and carried the
shield-with-WiFi-arcs
through the whole mark change. It is the one asset the author never
looks at,
and the one Google's SERP fetcher and older Safari prefer.
Now packed from the same PNGs as everything else, at 16/32/48. Written
by hand:
sharp cannot encode ICO and an ImageMagick dependency would not survive
CI. The
container is a 6-byte header and one 16-byte entry per image. 15,086
bytes → 553.
### The home page's social card 404'd
The card URL is assembled in two places holding different ids for the
same page
— the endpoint reads the content collection, which calls the English
home page
`index`; the `Head` override sees Starlight's route, whose id for it is
empty.
They agreed on 55 pages and disagreed on the one most links point at,
which
shipped as `og/.png`. Starlight also synthesises a 404 route with an
entry and
no collection page behind it, so that page pointed at a card nothing
rendered.
Both now resolve through one `cardPath`, and membership in the
collection — not
the presence of an entry — decides whether a page has a card.
**`check-social-cards.mjs`
reads every `og:image` out of the rendered HTML and asks the filesystem
whether
it is there.** It found the 404 defect within a second of being written.
### The print stylesheet shipped a 1.61:1 boundary
`@media print` was exempt from the contrast gate because "paper is one
background and the palette does not reach it". The first half is true;
the
second does not follow. Browsers do not print background colours by
default, so
on paper the `pre` border is the only thing separating a code block from
the
prose around it — a graphical object required to understand the content,
so
1.4.11 applies. `#ccc` → `#8a8a8a`, 1.61:1 → 3.45:1.
### Also
- **A social card per page** (W13) — 56 pages shared one banner. Text is
set in
the mono face the site already uses, which makes wrapping exact
arithmetic
rather than a guess, since librsvg does not measure text.
- **The i18n gate invented one mismatch and missed two** — `path="a.b"`
vs
`path={"a.b"}` keyed apart (the self-test asserted this, so the bug was
pinned
by its own suite); `<Home section />` and `<Home />` keyed identically;
a stray
`<Foo-Bar />` was reported as `Foo`.
- **Manifest and browser chrome follow the palette** (W14) — three
`#0e1316`
literals matched the token by coincidence. `theme-color` is also now
split in
two, since one dark value painted a dark address bar above a white page
for
every light-theme reader. New maskable icon: without one Android does
not crop
to the launcher shape, it shrinks the mark onto a plain white tile.
### Verification
Every fix is mutation-tested — reverting it makes exactly one named
check fail.
The brand rasters are byte-identical after the `brand-assets.mjs`
refactor,
which is the evidence that separating logic from IO changed no output.
Also checked, and closed with no change needed: the `picomatch` lockfile
churn
(`--frozen-lockfile` is in sync), the 753 KB `grafana-dashboard.png`
(referenced
from the JSON-LD, not stray), and CrowdSec's brand terms — they publish
no
trademark policy, their MIT carries no trademark clause, and this repo
ships no
CrowdSec or MikroTik logo. The exposure is the name used descriptively,
which is
what every third-party bouncer on their Hub does.
https://claude.ai/code/session_01Lt5tP3miz9YCv21qWsjBUo
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/jmrplens/cs-routeros-bouncer/pull/96?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<div id='description'>
<a href="https://bito.ai#summarystart"></a><h3>Summary by
Bito</h3><ul><li>Implemented per-page social cards generated at build
time, replacing the single shared image.</li>
<li>Updated browser theme colors and web manifest to dynamically follow
the site's color palette, improving accessibility and OS
integration.</li>
<li>Refactored the landing page to use a typed content contract,
ensuring consistency between human-readable text and machine-readable
structured data.</li>
<li>Fixed multiple documentation inaccuracies regarding binary behavior,
CLI paths, and configuration defaults by aligning them with the Go
source code.</li>
<li>Corrected broken binary download links and installation snippets by
dynamically resolving the latest release tag and fixing architecture
suffixes.</li>
</ul></div>
## Summary by Sourcery
Close remaining documentation and branding inconsistencies by generating
page-specific social assets, aligning browser metadata and content with
the product, and adding gates for the defects that previously escaped
validation.
New Features:
- Generate a distinct social card for each documentation page with
localized page titles and section labels.
- Add maskable app-icon support and synchronize browser and manifest
colors with the site palette.
Bug Fixes:
- Regenerate the legacy favicon from the current brand assets.
- Fix homepage and invalid-route social-card references so every
declared card resolves to a built file.
- Improve internationalization parity reporting for equivalent string
attributes, valueless attributes, and hyphenated component names.
- Correct print contrast for code-block borders and update inaccurate
documentation, installation commands, and download links.
Enhancements:
- Share brand and page-card content logic across raster generation and
build-time social-card rendering.
- Align architecture diagrams and landing-page content with the
documented and implemented product behavior.
CI:
- Add a build verification gate that checks all rendered social-image
references resolve to files.
Documentation:
- Refresh the changelog and user-facing documentation to reflect current
binary behavior, configuration, installation, and supported
functionality.
Tests:
- Strengthen contrast, manifest, internationalization, brand-asset, and
social-card validation, including mutation-oriented regression coverage.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added unique social cards for documentation pages, including localized
titles and branding.
- Added a maskable application icon and updated theme colors for light
and dark modes.
- Improved site metadata and branding across shared pages.
- **Bug Fixes**
- Fixed favicon generation and social-card routing.
- Improved print contrast for code blocks and links.
- Corrected internationalization component matching, including
hyphenated names.
- **Documentation**
- Documented social cards, architecture, firewall rules, logging,
configuration references, and updated branding.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
feat(docs): new brand mark, CrowdSec indigo, four diagrams, and the i18n gate on the MDX AST (#95)
## Description
Phases 5 to 7 of the documentation plan, plus the technical debt that
had accumulated in the i18n gate, plus a new brand mark chosen from
twenty-nine candidates.
## Type of change
- [x] ✨ New feature (non-breaking change that adds functionality)
- [x] 📖 Documentation update
- [x] 🧪 Tests / tooling / CI
- [x] 🧹 Refactoring / code cleanup
## Changes
### The i18n parity gate is rewritten onto the MDX AST
It had produced **three defects in one area** — an HTML-comment
terminator that missed `--!>`, a chained-replace sanitisation that left
a bare `<!--` behind, and a one-character backtick delimiter that leaked
double-backtick spans. Two were found by CodeQL, one in review. Three
findings in one file say the approach is wrong, not that the patches
were bad; parsing makes those classes unreachable.
All four parser packages were **already resolved in the lockfile**, so
declaring them added nothing to install.
Verified by a **differential harness over all 56 corpus files across six
dimensions**, which caught a regression the rewrite introduced and
surfaced a genuine improvement: three multi-path `ConfigOption`
invocations that used to collapse into one indistinguishable bucket are
now keyed individually. Demonstrated: a Spanish page pointing at a
non-existent config path **passed** the old gate and fails the new one.
It also corrected a claim the old file asserted: an HTML comment closed
with `--!>` is **not** closed for the renderer. CommonMark ends an HTML
block only at `-->`, so the earlier "fix" was wrong in the other
direction.
### A new mark
The shield is gone. Rendered side by side at the sizes that matter it
was, unmistakably, **the WiFi glyph** — a shield containing concentric
arcs radiating from a dot is the standard signal icon at 104px and at
23px. Nobody noticed until the candidates were laid out and looked at.
For a network tool that is worse than generic; it is misleading.
What replaces it: four lanes meeting a rail, three crossing and
continuing, one stopping dead at it. The proportion is deliberate — a
firewall does not block everything, it blocks the exception. **Three
primitives, 282 bytes of geometry, no ids, no defs, no mask**, against
the 3.8 KB the shield needed.
Chosen from 29 candidates across two waves. The first wave's five
directions all came from the data side — list, chain, loop, flow — and
its winner drew a generic list; the siblings anchor on a git ref and an
open book, objects that only exist in their domains. The second wave
entered through the domain and beat it on all four judging lenses.
### The accent moves to CrowdSec's indigo
`#4d4a98` light, `#807dc0` dark — the same hue and saturation raised
until it clears 4.5:1 against the **surface** rather than merely the
page. The page-only value failed twice, on the terminal prompt glyph and
the sidebar pill, and the contrast gate caught both.
This is ecosystem alignment, not a return to the co-branding retired
earlier: that palette paired CrowdSec indigo *with* MikroTik teal,
implying endorsement by two vendors including the hardware one. This is
a CrowdSec bouncer, listed on their Hub. **Worth checking their brand
guidelines before a release goes out.**
The rail takes the accent because the boundary is what the product is.
Pass and stop are carried by **length before colour**: the accent and
the muted tone measure 1.19:1 apart, so a mark relying on that pair to
tell them apart would come very close to collapsing into one tone in
greyscale.
CrowdSec's amber moves to `--rb-status-warn`, where it does semantic
work rather than decorating.
### Four architecture diagrams, under one convention
The rule-placement ladder, the decision funnel in three swimlanes, the
reconciliation diff and the shutdown order. Solid is a write, dashed is
a read, carried on three channels — line pattern, then colour, then a
legend inside each diagram — so they survive greyscale, colour vision
deficiency and a printout.
Six accuracy defects in them were corrected against the Go source. One
claim was rewritten rather than deleted: entries whose comment does not
match the prefix were described as "never listed", but `ListAddresses`
queries on the list name alone and filters client-side, so they **are**
fetched every pass and only then dropped — which costs transfer on a
list an operator may be sharing.
### Navigation
Sidebar depth hierarchy, pagination that names its destination (two
entries were both labelled "Overview"), scroll-into-view, and a **mobile
theme toggle** — there was none, the header hid the control below 50rem.
It never writes `data-theme` or `localStorage` itself: it drives
Starlight's own select and reads state back via a MutationObserver,
because anything else desynchronises from the desktop control and the
before-paint script.
## Testing
- [x] `pnpm analyze` exits 0 — config schema check, brand raster check,
i18n parity, contrast, `astro check`, ESLint, Prettier, build,
`html-validate`
- [x] `go build ./...`, `go test -race ./...`, golangci-lint, gosec,
staticcheck, govulncheck all clean; markdownlint clean
- [x] Every gate mutation-tested in both directions, including the
parity gate's twelve behaviours and the new brand-raster check
- [x] Rendered SVGs inspected in `dist/` — all 62 diagram edges parse,
dashed edges carry a real `stroke-dasharray`, and the mermaid CSS
ordering was checked so the dotted class actually wins
- [ ] Tested against a real MikroTik router (not applicable — no
behavioural change ships here)
## Notes for review
Four claims **of my own** were wrong and are corrected in the last
commit: a stale contrast figure in all three copies of the mark, the
`prefers-contrast` block still pointing at the retired teal, a comment
describing lanes "cut by the rail" that are unbroken rectangles, and a
false provenance claim about the sibling status fills' saturation.
An intermittent `analyze` failure reported by three agents did not
reproduce — 8 clean builds out of 8 in isolation. The cause was
concurrent agents building into the same `dist/`.
https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/jmrplens/cs-routeros-bouncer/pull/95?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Introduced refreshed CrowdSec Indigo branding across the documentation
site, including responsive wordmark, icons, and social images.
* Added light/dark theme support for diagrams and documentation images.
* Added mobile theme switching and improved sidebar, header, pagination,
and navigation behavior.
* Added clearer architecture diagrams, flow explanations, and Grafana
dashboard imagery.
* **Documentation**
* Expanded English and Spanish guidance for decision filtering,
firewall-rule placement, reconciliation, shutdown behavior, and decision
lifecycles.
* Clarified read/write diagram conventions and operational edge cases.
* **Bug Fixes**
* Improved validation for localized documentation structure and
generated brand assets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
feat: implement logging.file and configurable decision types, rebuild the docs (#94)
Phase 2 and 3 of the documentation plan, plus three items requested
directly. Every gate here was mutation-tested, because three of them
shipped failing open and were only caught that way.
logging.file is implemented. It was parsed and read by nothing; all
output went to stderr. Lines now go to BOTH stderr and the file, so
journalctl and docker logs keep working — the file is an addition, not a
redirection. Append, mode 0640 (log lines record banned clients' IP
addresses), parent directories deliberately not created, and a failure
to open is a startup error naming the path rather than a silent fallback.
A log file you configured and did not get is the failure class the
previous PR spent its time removing from the docs.
crowdsec.supported_decisions_types does something now. It was declared,
defaulted, mapped to an environment variable and read by nothing while
parseDecision hardcoded "ban". CrowdSec's decision type is a free string,
so a scenario emitting a custom type now has a way to be enforced. The
design follows what the Local API actually does, measured rather than
assumed: `type` matches exactly (type=ban,throttle returns zero) and
omitting it returns every type, so the default set keeps its server-side
filter and only a widened set fetches everything and filters locally.
The configuration reference is generated from the Go struct: a committed
artefact the docs render, regenerated and diff-checked in CI, retiring
110 hand-typed Env/Default lines across both locales. It immediately
caught six keys publishing defaults they do not have — per-protocol
rule-placement overrides inherit from the global placement rather than
from the constructor.
The token layer replaces a 681-line stylesheet with seven single-concern
sheets. Dark on bare :root, light on :root[data-theme="light"], every
colour token restated in both. The heading ladder is monotonic at every
viewport width now; it used to render h3 smaller than h4, and the first
fix left h5 and h6 below the body copy they introduce.
The landing is rebuilt from one HomeContent interface both locales must
satisfy, with the FAQ structured data generated from the same array as
the visible answers. Its new "What it writes to your router" section
publishes the limits on the front page, including a CPU figure measured
on the production router — a transient peaking at 29-34% against a 7%
baseline for about six seconds, in 11 of 11 cycles — together with the
warning that SNMP monitoring will not show it, because hrProcessorLoad
reports a one-minute average that flattens the spike to roughly 9%.
The firewall rules are single-sourced from internal/manager and rendered
on both the reference page and the landing. This corrected a
long-standing undercount: a stock configuration writes EIGHT rules, not
four. The two passthrough counting rules were absent from every listing
on the site, and they are written even when metrics.enabled is false.
Verified byte-for-byte against a production RB5009.
Three gates were failing open and now are not. The contrast gate's
symmetry check compared resolved palettes, so it was structurally blind
to a colour declared on bare :root and never restated for light. The
schema extractor still produced 93 keys after every SetDefault call was
deleted. The i18n gate could not see a section vanishing from one locale
when its heading stayed behind, so it now compares component
invocations.
Off-site documentation links use the canonical jmrp.io address the
repository homepage advertises; the Astro site keeps the Pages address it
is generated for.
Thirteen review threads addressed. Two CodeQL alerts in the gate scripts
themselves, both about hand-rolled markdown parsing, are fixed — that
file is scheduled to move onto the MDX AST.
Claude-Session: https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
feat: implement logging.file and configurable decision types, rebuild the docs (#94)
Phase 2 and 3 of the documentation plan, plus three items requested
directly. Every gate here was mutation-tested, because three of them
shipped failing open and were only caught that way.
logging.file is implemented. It was parsed and read by nothing; all
output went to stderr. Lines now go to BOTH stderr and the file, so
journalctl and docker logs keep working — the file is an addition, not a
redirection. Append, mode 0640 (log lines record banned clients' IP
addresses), parent directories deliberately not created, and a failure
to open is a startup error naming the path rather than a silent fallback.
A log file you configured and did not get is the failure class the
previous PR spent its time removing from the docs.
crowdsec.supported_decisions_types does something now. It was declared,
defaulted, mapped to an environment variable and read by nothing while
parseDecision hardcoded "ban". CrowdSec's decision type is a free string,
so a scenario emitting a custom type now has a way to be enforced. The
design follows what the Local API actually does, measured rather than
assumed: `type` matches exactly (type=ban,throttle returns zero) and
omitting it returns every type, so the default set keeps its server-side
filter and only a widened set fetches everything and filters locally.
The configuration reference is generated from the Go struct: a committed
artefact the docs render, regenerated and diff-checked in CI, retiring
110 hand-typed Env/Default lines across both locales. It immediately
caught six keys publishing defaults they do not have — per-protocol
rule-placement overrides inherit from the global placement rather than
from the constructor.
The token layer replaces a 681-line stylesheet with seven single-concern
sheets. Dark on bare :root, light on :root[data-theme="light"], every
colour token restated in both. The heading ladder is monotonic at every
viewport width now; it used to render h3 smaller than h4, and the first
fix left h5 and h6 below the body copy they introduce.
The landing is rebuilt from one HomeContent interface both locales must
satisfy, with the FAQ structured data generated from the same array as
the visible answers. Its new "What it writes to your router" section
publishes the limits on the front page, including a CPU figure measured
on the production router — a transient peaking at 29-34% against a 7%
baseline for about six seconds, in 11 of 11 cycles — together with the
warning that SNMP monitoring will not show it, because hrProcessorLoad
reports a one-minute average that flattens the spike to roughly 9%.
The firewall rules are single-sourced from internal/manager and rendered
on both the reference page and the landing. This corrected a
long-standing undercount: a stock configuration writes EIGHT rules, not
four. The two passthrough counting rules were absent from every listing
on the site, and they are written even when metrics.enabled is false.
Verified byte-for-byte against a production RB5009.
Three gates were failing open and now are not. The contrast gate's
symmetry check compared resolved palettes, so it was structurally blind
to a colour declared on bare :root and never restated for light. The
schema extractor still produced 93 keys after every SetDefault call was
deleted. The i18n gate could not see a section vanishing from one locale
when its heading stayed behind, so it now compares component
invocations.
Off-site documentation links use the canonical jmrp.io address the
repository homepage advertises; the Astro site keeps the Pages address it
is generated for.
Thirteen review threads addressed. Two CodeQL alerts in the gate scripts
themselves, both about hand-rolled markdown parsing, are fixed — that
file is scheduled to move onto the MDX AST.
Claude-Session: https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
feat(docs): new brand mark, CrowdSec indigo, four diagrams, and the i18n gate on the MDX AST (#95)
## Description
Phases 5 to 7 of the documentation plan, plus the technical debt that
had accumulated in the i18n gate, plus a new brand mark chosen from
twenty-nine candidates.
## Type of change
- [x] ✨ New feature (non-breaking change that adds functionality)
- [x] 📖 Documentation update
- [x] 🧪 Tests / tooling / CI
- [x] 🧹 Refactoring / code cleanup
## Changes
### The i18n parity gate is rewritten onto the MDX AST
It had produced **three defects in one area** — an HTML-comment
terminator that missed `--!>`, a chained-replace sanitisation that left
a bare `<!--` behind, and a one-character backtick delimiter that leaked
double-backtick spans. Two were found by CodeQL, one in review. Three
findings in one file say the approach is wrong, not that the patches
were bad; parsing makes those classes unreachable.
All four parser packages were **already resolved in the lockfile**, so
declaring them added nothing to install.
Verified by a **differential harness over all 56 corpus files across six
dimensions**, which caught a regression the rewrite introduced and
surfaced a genuine improvement: three multi-path `ConfigOption`
invocations that used to collapse into one indistinguishable bucket are
now keyed individually. Demonstrated: a Spanish page pointing at a
non-existent config path **passed** the old gate and fails the new one.
It also corrected a claim the old file asserted: an HTML comment closed
with `--!>` is **not** closed for the renderer. CommonMark ends an HTML
block only at `-->`, so the earlier "fix" was wrong in the other
direction.
### A new mark
The shield is gone. Rendered side by side at the sizes that matter it
was, unmistakably, **the WiFi glyph** — a shield containing concentric
arcs radiating from a dot is the standard signal icon at 104px and at
23px. Nobody noticed until the candidates were laid out and looked at.
For a network tool that is worse than generic; it is misleading.
What replaces it: four lanes meeting a rail, three crossing and
continuing, one stopping dead at it. The proportion is deliberate — a
firewall does not block everything, it blocks the exception. **Three
primitives, 282 bytes of geometry, no ids, no defs, no mask**, against
the 3.8 KB the shield needed.
Chosen from 29 candidates across two waves. The first wave's five
directions all came from the data side — list, chain, loop, flow — and
its winner drew a generic list; the siblings anchor on a git ref and an
open book, objects that only exist in their domains. The second wave
entered through the domain and beat it on all four judging lenses.
### The accent moves to CrowdSec's indigo
`#4d4a98` light, `#807dc0` dark — the same hue and saturation raised
until it clears 4.5:1 against the **surface** rather than merely the
page. The page-only value failed twice, on the terminal prompt glyph and
the sidebar pill, and the contrast gate caught both.
This is ecosystem alignment, not a return to the co-branding retired
earlier: that palette paired CrowdSec indigo *with* MikroTik teal,
implying endorsement by two vendors including the hardware one. This is
a CrowdSec bouncer, listed on their Hub. **Worth checking their brand
guidelines before a release goes out.**
The rail takes the accent because the boundary is what the product is.
Pass and stop are carried by **length before colour**: the accent and
the muted tone measure 1.19:1 apart, so a mark relying on that pair to
tell them apart would come very close to collapsing into one tone in
greyscale.
CrowdSec's amber moves to `--rb-status-warn`, where it does semantic
work rather than decorating.
### Four architecture diagrams, under one convention
The rule-placement ladder, the decision funnel in three swimlanes, the
reconciliation diff and the shutdown order. Solid is a write, dashed is
a read, carried on three channels — line pattern, then colour, then a
legend inside each diagram — so they survive greyscale, colour vision
deficiency and a printout.
Six accuracy defects in them were corrected against the Go source. One
claim was rewritten rather than deleted: entries whose comment does not
match the prefix were described as "never listed", but `ListAddresses`
queries on the list name alone and filters client-side, so they **are**
fetched every pass and only then dropped — which costs transfer on a
list an operator may be sharing.
### Navigation
Sidebar depth hierarchy, pagination that names its destination (two
entries were both labelled "Overview"), scroll-into-view, and a **mobile
theme toggle** — there was none, the header hid the control below 50rem.
It never writes `data-theme` or `localStorage` itself: it drives
Starlight's own select and reads state back via a MutationObserver,
because anything else desynchronises from the desktop control and the
before-paint script.
## Testing
- [x] `pnpm analyze` exits 0 — config schema check, brand raster check,
i18n parity, contrast, `astro check`, ESLint, Prettier, build,
`html-validate`
- [x] `go build ./...`, `go test -race ./...`, golangci-lint, gosec,
staticcheck, govulncheck all clean; markdownlint clean
- [x] Every gate mutation-tested in both directions, including the
parity gate's twelve behaviours and the new brand-raster check
- [x] Rendered SVGs inspected in `dist/` — all 62 diagram edges parse,
dashed edges carry a real `stroke-dasharray`, and the mermaid CSS
ordering was checked so the dotted class actually wins
- [ ] Tested against a real MikroTik router (not applicable — no
behavioural change ships here)
## Notes for review
Four claims **of my own** were wrong and are corrected in the last
commit: a stale contrast figure in all three copies of the mark, the
`prefers-contrast` block still pointing at the retired teal, a comment
describing lanes "cut by the rail" that are unbroken rectangles, and a
false provenance claim about the sibling status fills' saturation.
An intermittent `analyze` failure reported by three agents did not
reproduce — 8 clean builds out of 8 in isolation. The cause was
concurrent agents building into the same `dist/`.
https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/jmrplens/cs-routeros-bouncer/pull/95?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Introduced refreshed CrowdSec Indigo branding across the documentation
site, including responsive wordmark, icons, and social images.
* Added light/dark theme support for diagrams and documentation images.
* Added mobile theme switching and improved sidebar, header, pagination,
and navigation behavior.
* Added clearer architecture diagrams, flow explanations, and Grafana
dashboard imagery.
* **Documentation**
* Expanded English and Spanish guidance for decision filtering,
firewall-rule placement, reconciliation, shutdown behavior, and decision
lifecycles.
* Clarified read/write diagram conventions and operational edge cases.
* **Bug Fixes**
* Improved validation for localized documentation structure and
generated brand assets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
feat: implement logging.file and configurable decision types, rebuild the docs (#94)
Phase 2 and 3 of the documentation plan, plus three items requested
directly. Every gate here was mutation-tested, because three of them
shipped failing open and were only caught that way.
logging.file is implemented. It was parsed and read by nothing; all
output went to stderr. Lines now go to BOTH stderr and the file, so
journalctl and docker logs keep working — the file is an addition, not a
redirection. Append, mode 0640 (log lines record banned clients' IP
addresses), parent directories deliberately not created, and a failure
to open is a startup error naming the path rather than a silent fallback.
A log file you configured and did not get is the failure class the
previous PR spent its time removing from the docs.
crowdsec.supported_decisions_types does something now. It was declared,
defaulted, mapped to an environment variable and read by nothing while
parseDecision hardcoded "ban". CrowdSec's decision type is a free string,
so a scenario emitting a custom type now has a way to be enforced. The
design follows what the Local API actually does, measured rather than
assumed: `type` matches exactly (type=ban,throttle returns zero) and
omitting it returns every type, so the default set keeps its server-side
filter and only a widened set fetches everything and filters locally.
The configuration reference is generated from the Go struct: a committed
artefact the docs render, regenerated and diff-checked in CI, retiring
110 hand-typed Env/Default lines across both locales. It immediately
caught six keys publishing defaults they do not have — per-protocol
rule-placement overrides inherit from the global placement rather than
from the constructor.
The token layer replaces a 681-line stylesheet with seven single-concern
sheets. Dark on bare :root, light on :root[data-theme="light"], every
colour token restated in both. The heading ladder is monotonic at every
viewport width now; it used to render h3 smaller than h4, and the first
fix left h5 and h6 below the body copy they introduce.
The landing is rebuilt from one HomeContent interface both locales must
satisfy, with the FAQ structured data generated from the same array as
the visible answers. Its new "What it writes to your router" section
publishes the limits on the front page, including a CPU figure measured
on the production router — a transient peaking at 29-34% against a 7%
baseline for about six seconds, in 11 of 11 cycles — together with the
warning that SNMP monitoring will not show it, because hrProcessorLoad
reports a one-minute average that flattens the spike to roughly 9%.
The firewall rules are single-sourced from internal/manager and rendered
on both the reference page and the landing. This corrected a
long-standing undercount: a stock configuration writes EIGHT rules, not
four. The two passthrough counting rules were absent from every listing
on the site, and they are written even when metrics.enabled is false.
Verified byte-for-byte against a production RB5009.
Three gates were failing open and now are not. The contrast gate's
symmetry check compared resolved palettes, so it was structurally blind
to a colour declared on bare :root and never restated for light. The
schema extractor still produced 93 keys after every SetDefault call was
deleted. The i18n gate could not see a section vanishing from one locale
when its heading stayed behind, so it now compares component
invocations.
Off-site documentation links use the canonical jmrp.io address the
repository homepage advertises; the Astro site keeps the Pages address it
is generated for.
Thirteen review threads addressed. Two CodeQL alerts in the gate scripts
themselves, both about hand-rolled markdown parsing, are fixed — that
file is scheduled to move onto the MDX AST.
Claude-Session: https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
feat: implement logging.file and configurable decision types, rebuild the docs (#94)
Phase 2 and 3 of the documentation plan, plus three items requested
directly. Every gate here was mutation-tested, because three of them
shipped failing open and were only caught that way.
logging.file is implemented. It was parsed and read by nothing; all
output went to stderr. Lines now go to BOTH stderr and the file, so
journalctl and docker logs keep working — the file is an addition, not a
redirection. Append, mode 0640 (log lines record banned clients' IP
addresses), parent directories deliberately not created, and a failure
to open is a startup error naming the path rather than a silent fallback.
A log file you configured and did not get is the failure class the
previous PR spent its time removing from the docs.
crowdsec.supported_decisions_types does something now. It was declared,
defaulted, mapped to an environment variable and read by nothing while
parseDecision hardcoded "ban". CrowdSec's decision type is a free string,
so a scenario emitting a custom type now has a way to be enforced. The
design follows what the Local API actually does, measured rather than
assumed: `type` matches exactly (type=ban,throttle returns zero) and
omitting it returns every type, so the default set keeps its server-side
filter and only a widened set fetches everything and filters locally.
The configuration reference is generated from the Go struct: a committed
artefact the docs render, regenerated and diff-checked in CI, retiring
110 hand-typed Env/Default lines across both locales. It immediately
caught six keys publishing defaults they do not have — per-protocol
rule-placement overrides inherit from the global placement rather than
from the constructor.
The token layer replaces a 681-line stylesheet with seven single-concern
sheets. Dark on bare :root, light on :root[data-theme="light"], every
colour token restated in both. The heading ladder is monotonic at every
viewport width now; it used to render h3 smaller than h4, and the first
fix left h5 and h6 below the body copy they introduce.
The landing is rebuilt from one HomeContent interface both locales must
satisfy, with the FAQ structured data generated from the same array as
the visible answers. Its new "What it writes to your router" section
publishes the limits on the front page, including a CPU figure measured
on the production router — a transient peaking at 29-34% against a 7%
baseline for about six seconds, in 11 of 11 cycles — together with the
warning that SNMP monitoring will not show it, because hrProcessorLoad
reports a one-minute average that flattens the spike to roughly 9%.
The firewall rules are single-sourced from internal/manager and rendered
on both the reference page and the landing. This corrected a
long-standing undercount: a stock configuration writes EIGHT rules, not
four. The two passthrough counting rules were absent from every listing
on the site, and they are written even when metrics.enabled is false.
Verified byte-for-byte against a production RB5009.
Three gates were failing open and now are not. The contrast gate's
symmetry check compared resolved palettes, so it was structurally blind
to a colour declared on bare :root and never restated for light. The
schema extractor still produced 93 keys after every SetDefault call was
deleted. The i18n gate could not see a section vanishing from one locale
when its heading stayed behind, so it now compares component
invocations.
Off-site documentation links use the canonical jmrp.io address the
repository homepage advertises; the Astro site keeps the Pages address it
is generated for.
Thirteen review threads addressed. Two CodeQL alerts in the gate scripts
themselves, both about hand-rolled markdown parsing, are fixed — that
file is scheduled to move onto the MDX AST.
Claude-Session: https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
feat(docs): new brand mark, CrowdSec indigo, four diagrams, and the i18n gate on the MDX AST (#95)
## Description
Phases 5 to 7 of the documentation plan, plus the technical debt that
had accumulated in the i18n gate, plus a new brand mark chosen from
twenty-nine candidates.
## Type of change
- [x] ✨ New feature (non-breaking change that adds functionality)
- [x] 📖 Documentation update
- [x] 🧪 Tests / tooling / CI
- [x] 🧹 Refactoring / code cleanup
## Changes
### The i18n parity gate is rewritten onto the MDX AST
It had produced **three defects in one area** — an HTML-comment
terminator that missed `--!>`, a chained-replace sanitisation that left
a bare `<!--` behind, and a one-character backtick delimiter that leaked
double-backtick spans. Two were found by CodeQL, one in review. Three
findings in one file say the approach is wrong, not that the patches
were bad; parsing makes those classes unreachable.
All four parser packages were **already resolved in the lockfile**, so
declaring them added nothing to install.
Verified by a **differential harness over all 56 corpus files across six
dimensions**, which caught a regression the rewrite introduced and
surfaced a genuine improvement: three multi-path `ConfigOption`
invocations that used to collapse into one indistinguishable bucket are
now keyed individually. Demonstrated: a Spanish page pointing at a
non-existent config path **passed** the old gate and fails the new one.
It also corrected a claim the old file asserted: an HTML comment closed
with `--!>` is **not** closed for the renderer. CommonMark ends an HTML
block only at `-->`, so the earlier "fix" was wrong in the other
direction.
### A new mark
The shield is gone. Rendered side by side at the sizes that matter it
was, unmistakably, **the WiFi glyph** — a shield containing concentric
arcs radiating from a dot is the standard signal icon at 104px and at
23px. Nobody noticed until the candidates were laid out and looked at.
For a network tool that is worse than generic; it is misleading.
What replaces it: four lanes meeting a rail, three crossing and
continuing, one stopping dead at it. The proportion is deliberate — a
firewall does not block everything, it blocks the exception. **Three
primitives, 282 bytes of geometry, no ids, no defs, no mask**, against
the 3.8 KB the shield needed.
Chosen from 29 candidates across two waves. The first wave's five
directions all came from the data side — list, chain, loop, flow — and
its winner drew a generic list; the siblings anchor on a git ref and an
open book, objects that only exist in their domains. The second wave
entered through the domain and beat it on all four judging lenses.
### The accent moves to CrowdSec's indigo
`#4d4a98` light, `#807dc0` dark — the same hue and saturation raised
until it clears 4.5:1 against the **surface** rather than merely the
page. The page-only value failed twice, on the terminal prompt glyph and
the sidebar pill, and the contrast gate caught both.
This is ecosystem alignment, not a return to the co-branding retired
earlier: that palette paired CrowdSec indigo *with* MikroTik teal,
implying endorsement by two vendors including the hardware one. This is
a CrowdSec bouncer, listed on their Hub. **Worth checking their brand
guidelines before a release goes out.**
The rail takes the accent because the boundary is what the product is.
Pass and stop are carried by **length before colour**: the accent and
the muted tone measure 1.19:1 apart, so a mark relying on that pair to
tell them apart would come very close to collapsing into one tone in
greyscale.
CrowdSec's amber moves to `--rb-status-warn`, where it does semantic
work rather than decorating.
### Four architecture diagrams, under one convention
The rule-placement ladder, the decision funnel in three swimlanes, the
reconciliation diff and the shutdown order. Solid is a write, dashed is
a read, carried on three channels — line pattern, then colour, then a
legend inside each diagram — so they survive greyscale, colour vision
deficiency and a printout.
Six accuracy defects in them were corrected against the Go source. One
claim was rewritten rather than deleted: entries whose comment does not
match the prefix were described as "never listed", but `ListAddresses`
queries on the list name alone and filters client-side, so they **are**
fetched every pass and only then dropped — which costs transfer on a
list an operator may be sharing.
### Navigation
Sidebar depth hierarchy, pagination that names its destination (two
entries were both labelled "Overview"), scroll-into-view, and a **mobile
theme toggle** — there was none, the header hid the control below 50rem.
It never writes `data-theme` or `localStorage` itself: it drives
Starlight's own select and reads state back via a MutationObserver,
because anything else desynchronises from the desktop control and the
before-paint script.
## Testing
- [x] `pnpm analyze` exits 0 — config schema check, brand raster check,
i18n parity, contrast, `astro check`, ESLint, Prettier, build,
`html-validate`
- [x] `go build ./...`, `go test -race ./...`, golangci-lint, gosec,
staticcheck, govulncheck all clean; markdownlint clean
- [x] Every gate mutation-tested in both directions, including the
parity gate's twelve behaviours and the new brand-raster check
- [x] Rendered SVGs inspected in `dist/` — all 62 diagram edges parse,
dashed edges carry a real `stroke-dasharray`, and the mermaid CSS
ordering was checked so the dotted class actually wins
- [ ] Tested against a real MikroTik router (not applicable — no
behavioural change ships here)
## Notes for review
Four claims **of my own** were wrong and are corrected in the last
commit: a stale contrast figure in all three copies of the mark, the
`prefers-contrast` block still pointing at the retired teal, a comment
describing lanes "cut by the rail" that are unbroken rectangles, and a
false provenance claim about the sibling status fills' saturation.
An intermittent `analyze` failure reported by three agents did not
reproduce — 8 clean builds out of 8 in isolation. The cause was
concurrent agents building into the same `dist/`.
https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/jmrplens/cs-routeros-bouncer/pull/95?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Introduced refreshed CrowdSec Indigo branding across the documentation
site, including responsive wordmark, icons, and social images.
* Added light/dark theme support for diagrams and documentation images.
* Added mobile theme switching and improved sidebar, header, pagination,
and navigation behavior.
* Added clearer architecture diagrams, flow explanations, and Grafana
dashboard imagery.
* **Documentation**
* Expanded English and Spanish guidance for decision filtering,
firewall-rule placement, reconciliation, shutdown behavior, and decision
lifecycles.
* Clarified read/write diagram conventions and operational edge cases.
* **Bug Fixes**
* Improved validation for localized documentation structure and
generated brand assets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
feat: implement logging.file and configurable decision types, rebuild the docs (#94)
Phase 2 and 3 of the documentation plan, plus three items requested
directly. Every gate here was mutation-tested, because three of them
shipped failing open and were only caught that way.
logging.file is implemented. It was parsed and read by nothing; all
output went to stderr. Lines now go to BOTH stderr and the file, so
journalctl and docker logs keep working — the file is an addition, not a
redirection. Append, mode 0640 (log lines record banned clients' IP
addresses), parent directories deliberately not created, and a failure
to open is a startup error naming the path rather than a silent fallback.
A log file you configured and did not get is the failure class the
previous PR spent its time removing from the docs.
crowdsec.supported_decisions_types does something now. It was declared,
defaulted, mapped to an environment variable and read by nothing while
parseDecision hardcoded "ban". CrowdSec's decision type is a free string,
so a scenario emitting a custom type now has a way to be enforced. The
design follows what the Local API actually does, measured rather than
assumed: `type` matches exactly (type=ban,throttle returns zero) and
omitting it returns every type, so the default set keeps its server-side
filter and only a widened set fetches everything and filters locally.
The configuration reference is generated from the Go struct: a committed
artefact the docs render, regenerated and diff-checked in CI, retiring
110 hand-typed Env/Default lines across both locales. It immediately
caught six keys publishing defaults they do not have — per-protocol
rule-placement overrides inherit from the global placement rather than
from the constructor.
The token layer replaces a 681-line stylesheet with seven single-concern
sheets. Dark on bare :root, light on :root[data-theme="light"], every
colour token restated in both. The heading ladder is monotonic at every
viewport width now; it used to render h3 smaller than h4, and the first
fix left h5 and h6 below the body copy they introduce.
The landing is rebuilt from one HomeContent interface both locales must
satisfy, with the FAQ structured data generated from the same array as
the visible answers. Its new "What it writes to your router" section
publishes the limits on the front page, including a CPU figure measured
on the production router — a transient peaking at 29-34% against a 7%
baseline for about six seconds, in 11 of 11 cycles — together with the
warning that SNMP monitoring will not show it, because hrProcessorLoad
reports a one-minute average that flattens the spike to roughly 9%.
The firewall rules are single-sourced from internal/manager and rendered
on both the reference page and the landing. This corrected a
long-standing undercount: a stock configuration writes EIGHT rules, not
four. The two passthrough counting rules were absent from every listing
on the site, and they are written even when metrics.enabled is false.
Verified byte-for-byte against a production RB5009.
Three gates were failing open and now are not. The contrast gate's
symmetry check compared resolved palettes, so it was structurally blind
to a colour declared on bare :root and never restated for light. The
schema extractor still produced 93 keys after every SetDefault call was
deleted. The i18n gate could not see a section vanishing from one locale
when its heading stayed behind, so it now compares component
invocations.
Off-site documentation links use the canonical jmrp.io address the
repository homepage advertises; the Astro site keeps the Pages address it
is generated for.
Thirteen review threads addressed. Two CodeQL alerts in the gate scripts
themselves, both about hand-rolled markdown parsing, are fixed — that
file is scheduled to move onto the MDX AST.
Claude-Session: https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
feat(docs): new brand mark, CrowdSec indigo, four diagrams, and the i18n gate on the MDX AST (#95)
## Description
Phases 5 to 7 of the documentation plan, plus the technical debt that
had accumulated in the i18n gate, plus a new brand mark chosen from
twenty-nine candidates.
## Type of change
- [x] ✨ New feature (non-breaking change that adds functionality)
- [x] 📖 Documentation update
- [x] 🧪 Tests / tooling / CI
- [x] 🧹 Refactoring / code cleanup
## Changes
### The i18n parity gate is rewritten onto the MDX AST
It had produced **three defects in one area** — an HTML-comment
terminator that missed `--!>`, a chained-replace sanitisation that left
a bare `<!--` behind, and a one-character backtick delimiter that leaked
double-backtick spans. Two were found by CodeQL, one in review. Three
findings in one file say the approach is wrong, not that the patches
were bad; parsing makes those classes unreachable.
All four parser packages were **already resolved in the lockfile**, so
declaring them added nothing to install.
Verified by a **differential harness over all 56 corpus files across six
dimensions**, which caught a regression the rewrite introduced and
surfaced a genuine improvement: three multi-path `ConfigOption`
invocations that used to collapse into one indistinguishable bucket are
now keyed individually. Demonstrated: a Spanish page pointing at a
non-existent config path **passed** the old gate and fails the new one.
It also corrected a claim the old file asserted: an HTML comment closed
with `--!>` is **not** closed for the renderer. CommonMark ends an HTML
block only at `-->`, so the earlier "fix" was wrong in the other
direction.
### A new mark
The shield is gone. Rendered side by side at the sizes that matter it
was, unmistakably, **the WiFi glyph** — a shield containing concentric
arcs radiating from a dot is the standard signal icon at 104px and at
23px. Nobody noticed until the candidates were laid out and looked at.
For a network tool that is worse than generic; it is misleading.
What replaces it: four lanes meeting a rail, three crossing and
continuing, one stopping dead at it. The proportion is deliberate — a
firewall does not block everything, it blocks the exception. **Three
primitives, 282 bytes of geometry, no ids, no defs, no mask**, against
the 3.8 KB the shield needed.
Chosen from 29 candidates across two waves. The first wave's five
directions all came from the data side — list, chain, loop, flow — and
its winner drew a generic list; the siblings anchor on a git ref and an
open book, objects that only exist in their domains. The second wave
entered through the domain and beat it on all four judging lenses.
### The accent moves to CrowdSec's indigo
`#4d4a98` light, `#807dc0` dark — the same hue and saturation raised
until it clears 4.5:1 against the **surface** rather than merely the
page. The page-only value failed twice, on the terminal prompt glyph and
the sidebar pill, and the contrast gate caught both.
This is ecosystem alignment, not a return to the co-branding retired
earlier: that palette paired CrowdSec indigo *with* MikroTik teal,
implying endorsement by two vendors including the hardware one. This is
a CrowdSec bouncer, listed on their Hub. **Worth checking their brand
guidelines before a release goes out.**
The rail takes the accent because the boundary is what the product is.
Pass and stop are carried by **length before colour**: the accent and
the muted tone measure 1.19:1 apart, so a mark relying on that pair to
tell them apart would come very close to collapsing into one tone in
greyscale.
CrowdSec's amber moves to `--rb-status-warn`, where it does semantic
work rather than decorating.
### Four architecture diagrams, under one convention
The rule-placement ladder, the decision funnel in three swimlanes, the
reconciliation diff and the shutdown order. Solid is a write, dashed is
a read, carried on three channels — line pattern, then colour, then a
legend inside each diagram — so they survive greyscale, colour vision
deficiency and a printout.
Six accuracy defects in them were corrected against the Go source. One
claim was rewritten rather than deleted: entries whose comment does not
match the prefix were described as "never listed", but `ListAddresses`
queries on the list name alone and filters client-side, so they **are**
fetched every pass and only then dropped — which costs transfer on a
list an operator may be sharing.
### Navigation
Sidebar depth hierarchy, pagination that names its destination (two
entries were both labelled "Overview"), scroll-into-view, and a **mobile
theme toggle** — there was none, the header hid the control below 50rem.
It never writes `data-theme` or `localStorage` itself: it drives
Starlight's own select and reads state back via a MutationObserver,
because anything else desynchronises from the desktop control and the
before-paint script.
## Testing
- [x] `pnpm analyze` exits 0 — config schema check, brand raster check,
i18n parity, contrast, `astro check`, ESLint, Prettier, build,
`html-validate`
- [x] `go build ./...`, `go test -race ./...`, golangci-lint, gosec,
staticcheck, govulncheck all clean; markdownlint clean
- [x] Every gate mutation-tested in both directions, including the
parity gate's twelve behaviours and the new brand-raster check
- [x] Rendered SVGs inspected in `dist/` — all 62 diagram edges parse,
dashed edges carry a real `stroke-dasharray`, and the mermaid CSS
ordering was checked so the dotted class actually wins
- [ ] Tested against a real MikroTik router (not applicable — no
behavioural change ships here)
## Notes for review
Four claims **of my own** were wrong and are corrected in the last
commit: a stale contrast figure in all three copies of the mark, the
`prefers-contrast` block still pointing at the retired teal, a comment
describing lanes "cut by the rail" that are unbroken rectangles, and a
false provenance claim about the sibling status fills' saturation.
An intermittent `analyze` failure reported by three agents did not
reproduce — 8 clean builds out of 8 in isolation. The cause was
concurrent agents building into the same `dist/`.
https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/jmrplens/cs-routeros-bouncer/pull/95?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Introduced refreshed CrowdSec Indigo branding across the documentation
site, including responsive wordmark, icons, and social images.
* Added light/dark theme support for diagrams and documentation images.
* Added mobile theme switching and improved sidebar, header, pagination,
and navigation behavior.
* Added clearer architecture diagrams, flow explanations, and Grafana
dashboard imagery.
* **Documentation**
* Expanded English and Spanish guidance for decision filtering,
firewall-rule placement, reconciliation, shutdown behavior, and decision
lifecycles.
* Clarified read/write diagram conventions and operational edge cases.
* **Bug Fixes**
* Improved validation for localized documentation structure and
generated brand assets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
feat: implement logging.file and configurable decision types, rebuild the docs (#94)
Phase 2 and 3 of the documentation plan, plus three items requested
directly. Every gate here was mutation-tested, because three of them
shipped failing open and were only caught that way.
logging.file is implemented. It was parsed and read by nothing; all
output went to stderr. Lines now go to BOTH stderr and the file, so
journalctl and docker logs keep working — the file is an addition, not a
redirection. Append, mode 0640 (log lines record banned clients' IP
addresses), parent directories deliberately not created, and a failure
to open is a startup error naming the path rather than a silent fallback.
A log file you configured and did not get is the failure class the
previous PR spent its time removing from the docs.
crowdsec.supported_decisions_types does something now. It was declared,
defaulted, mapped to an environment variable and read by nothing while
parseDecision hardcoded "ban". CrowdSec's decision type is a free string,
so a scenario emitting a custom type now has a way to be enforced. The
design follows what the Local API actually does, measured rather than
assumed: `type` matches exactly (type=ban,throttle returns zero) and
omitting it returns every type, so the default set keeps its server-side
filter and only a widened set fetches everything and filters locally.
The configuration reference is generated from the Go struct: a committed
artefact the docs render, regenerated and diff-checked in CI, retiring
110 hand-typed Env/Default lines across both locales. It immediately
caught six keys publishing defaults they do not have — per-protocol
rule-placement overrides inherit from the global placement rather than
from the constructor.
The token layer replaces a 681-line stylesheet with seven single-concern
sheets. Dark on bare :root, light on :root[data-theme="light"], every
colour token restated in both. The heading ladder is monotonic at every
viewport width now; it used to render h3 smaller than h4, and the first
fix left h5 and h6 below the body copy they introduce.
The landing is rebuilt from one HomeContent interface both locales must
satisfy, with the FAQ structured data generated from the same array as
the visible answers. Its new "What it writes to your router" section
publishes the limits on the front page, including a CPU figure measured
on the production router — a transient peaking at 29-34% against a 7%
baseline for about six seconds, in 11 of 11 cycles — together with the
warning that SNMP monitoring will not show it, because hrProcessorLoad
reports a one-minute average that flattens the spike to roughly 9%.
The firewall rules are single-sourced from internal/manager and rendered
on both the reference page and the landing. This corrected a
long-standing undercount: a stock configuration writes EIGHT rules, not
four. The two passthrough counting rules were absent from every listing
on the site, and they are written even when metrics.enabled is false.
Verified byte-for-byte against a production RB5009.
Three gates were failing open and now are not. The contrast gate's
symmetry check compared resolved palettes, so it was structurally blind
to a colour declared on bare :root and never restated for light. The
schema extractor still produced 93 keys after every SetDefault call was
deleted. The i18n gate could not see a section vanishing from one locale
when its heading stayed behind, so it now compares component
invocations.
Off-site documentation links use the canonical jmrp.io address the
repository homepage advertises; the Astro site keeps the Pages address it
is generated for.
Thirteen review threads addressed. Two CodeQL alerts in the gate scripts
themselves, both about hand-rolled markdown parsing, are fixed — that
file is scheduled to move onto the MDX AST.
Claude-Session: https://claude.ai/code/session_01ENguejGi5gZcxoMbCKWFBy