WHY .gitignore "was not working": it was working. git never consults
.gitignore for a file it ALREADY TRACKS. The rule `*secret*` matched fine --
proved by dropping an untracked copy in and watching check-ignore name
`.gitignore:3:*secret*`. The file had been tracked since feea0606, which is
ironically the commit that de-gitted the Resend key.
WHAT THE EXPOSED VALUE ACTUALLY WAS. Not an analytics password: the GITEA
ADMIN ACCOUNT PASSWORD (is_admin true; /api/v1/admin/users answered 200), in
a repo gitea.dooplex.hu serves anonymously to the internet. That is push
access to every repo -- including the one whose website/ is git-synced live
and whose scripts/ is published by tag to every new box installer (R-110).
Re-ranked P2 -> P1 on that measurement; my first ranking had only measured
the analytics blast radius.
Every committed value was still live. Nothing had ever been rotated.
ROTATED (values never echoed; written to a 0600 file on DooPlex):
umami-config APP_SECRET + POSTGRES_PASSWORD. The password was
changed INSIDE postgres (ALTER USER) as well as in the
Secret -- the env var is only read at first init, so
patching the Secret alone would have changed nothing.
healthchecks-config SECRET_KEY + SUPERUSER_PASSWORD (nothing consumes them,
there is no healthchecks Deployment).
gitea-creds no longer holds the admin password at all: a SCOPED
token (read:package + read:repository).
gitea admin new random password; gitea-system/gitea-admin updated.
VERIFIED, not assumed:
- new admin password -> 200, OLD PUBLISHED PASSWORD -> 401 (the leak is dead)
- umami: a real beacon returns 200 (so the app authenticates to postgres and
writes) while a bogus site id still returns 400 (so the 200 means something)
- hub: "Registry version check: latest = 0.304.0" AND "Template fetched
(5881 bytes)", no auth failures
- BOTH token scopes are load-bearing, and the second was found by breaking
it: a package-only token made the hub log "Template fetch: unexpected
status 403", because the template fetcher reads a raw file out of the
felhom-controller repo, not the registry.
AN INCIDENT CAUSED BY THE FIX, recorded because it is the useful part: the
rollout restart needed to pick up the new umami secret put umami into
CrashLoopBackOff and took stats.felhom.eu down (503) for ~4 minutes. Not the
rotation -- at memory 512Mi that pod runs for months but CANNOT RESTART:
startup (Prisma + Next.js) peaks over the limit and is OOMKilled (exit 137).
Raised to 1Gi IN THE MANIFEST, not just live, per .claude/rules/manifests.md
("never bare kubectl set -- the next sync reverts it and the fix silently
disappears").
THE GATE: KNOWN_BACKLOG is removed from manifest_bearer_gate.py, as its own
comment instructed. Red-proofed with a decoy: exit 1 with it, exit 0 without.
An exemption kept this visible for three months and changed nothing.
WHAT REMAINS (operator, and it is bigger than what was fixed): the same
password is still the admin password in ~12 other namespaces -- nextcloud,
paperless, bookstack (a DATABASE ROOT password), tandoor, calibre,
adventurelog, gokapi, qbittorrent, servarr, homepage. Rotating Gitea does not
touch them. Also owed: a kisfenyo Gitea token sits in plaintext in the local
homelab-manifests remote URL and was printed to a session transcript during
this investigation, so it should be replaced regardless (R-580's shape).
NOT a finding: homelab-manifests is private (404 anonymously) and does not
contain the password; ArgoCD's repo credential is a separate token and was
untouched by the rotation.
Found while publishing the legal pages: a background security review flagged
the <!-- source --> comments served in website/adatkezeles.html. Chasing what
those comments point AT found something larger.
MEASURED, and the control is what makes it mean anything:
- manifest_bearer_gate.py itself prints
"manifests/felhom.secret.yaml:39 KNOWN-BACKLOG committed secret"
- that file holds live-shaped values, not placeholders: SECRET_KEY (69),
SUPERUSER_PASSWORD (18), Umami APP_SECRET (66) and POSTGRES_PASSWORD (34),
plus a username/password pair. Values were never printed, only measured
by length.
- gitea.dooplex.hu resolves to 37.191.56.193, the website's public address
- anonymous curl: 200 and 1686 bytes for that file; 200 for hub internals
- OFF-NETWORK CONTROL: fetched from outside the operator's network, a real
path returns the file's first line and a nonsense path returns 404. So
the 200 is genuine anonymous read from the internet, not a LAN-only ACL.
This contradicts documentation/runbooks/secrets.md:3-4, which states secret
values are never committed and the manifests carry only placeholders. That
promise is false today.
Ranked P2, not P1, and the row says why so the operator can overrule: the
exposed credentials guard analytics and an undeployed healthchecks instance,
NOT household data. Measured: umami-db is a ClusterIP service with no
external IP, so the Postgres password is not internet-reachable. No customer
box, hub token or escrow key is in the file.
NOTHING WAS CHANGED. Making the repo private could break the public day-0
installer path (R-110), and rotation plus repo visibility are operator
decisions on production infrastructure. The row carries the order: rotate
first (de-git alone kills nothing — the runbook says so), then decide
visibility, then CC does the de-git and tightens the gate.
Coupled and easy to miss: the 32 source comments on /adatkezeles are harmless
while the repo is public, but become a map of it the moment it is private, so
that is one change and not two. Their traceability is already kept in
documentation/legal/*-1.0.md.
Two new Hungarian pages, live on push: /adatkezeles (privacy notice) and
/feltetelek (what the free closed test is, and is not). Operator rulings of
2026-10-09: no company exists yet, so the operator is named as a PRIVATE
PERSON with no postal address and no phone; publish before the lawyer has
seen it, because the site was collecting data with no notice at all; the full
ASZF, the impresszum and the review wait for the company (R-802, R-809).
- All 18 pages carry the operator, info@felhom.eu and both links in the
footer. Counted, not assumed: 18 pages found = 18 with both links = 18
structurally valid. English footers say the legal texts are Hungarian.
- The contact form stopped claiming something untrue. The old consent said
"az adatokat harmadik felnek nem adjuk ki" while Resend, Cloudflare and
Google carry the message. Both languages replaced; the link opens in a new
tab so a filled form is not lost.
- site_gates.py NO_TWIN gains the two pages ON PURPOSE, with the reason in a
comment: an unreviewed English legal text would be worse than an honest
pointer from the English footer.
- documentation/legal/{adatkezelesi-tajekoztato,feltetelek}-1.0.md are the
text of record, DERIVED from the published HTML so they cannot drift. The
drafts are kept and marked superseded for the closed test.
FOUR LOAD-BEARING CLAIMS WERE MEASURED, not copied from a vendor or a README:
cookies zero, and no local storage - checked in the browser WITH A
POSITIVE CONTROL (a probe cookie WAS visible to the same
method) after the tracker fired; no Set-Cookie on any response
beacon the exact Umami payload: site id, screen, language, title,
url, referrer - no visitor identifier
Cloudflare DNS only: the public A record 37.191.56.193 is not a
Cloudflare address, so site traffic cannot be proxied
fsn1 Falkenstein, Germany (Hetzner's own location list)
Also measured: felhom-ep0-copy-gc.timer is installed and RAN SUCCESSFULLY
(2026-10-09 08:00, exit 0), so "deleted within 30 days" is true today where
on 2026-10-08 it was written but not switched on.
Three retentions are stated as having NO deadline, deliberately and with the
operator's word: website statistics, web server logs and contact messages
have no automatic deletion, and the pages say so instead of promising a date
nothing enforces. Every other period is enforced by configuration and cited.
No placeholder survived onto either page (0 of "[[", control: the draft still
has 36). The impresszum and the full ASZF are NOT published.
R-813 -> NARROWED. R-915 unblocked: the operator enters the two URLs in the
Meta app's Basic settings; the Live switch stays a separate decision.
R-917 and R-920 -> DEFERRED on the operator's (c): park, publish as posts
once posting starts.
The task asked for a hex compare between what is pasted into Messenger and
COPY.md section 5. There was nothing to paste (R-920), so the half that is
still checkable was checked:
all four questions BYTE-IDENTICAL to website/gyik.html (hex equal)
each answer at most three sentences (2/3/3/3), each ending in the gyik link
A refused edit is not a reason to leave the copy unverified.
Same two Windows-only gate failures as the previous commit (instructions: the
Windows workspace CLAUDE.md is MEANT to diverge from the DooPlex-shaped
versioned copy; script-tests: WinError 32, no sqlite3 CLI, POSIX shell tests).
All 18 gates are green on DooPlex at 97d3c29f9c, run in a throwaway worktree
beside the sibling repos, and CI run #863 for that commit is Success.
unproven.py --summary unchanged: 35 of 55 not walked.
Second Facebook task of the day. Page settings changed by hand in the operator's
Chrome; every edit read back from a channel other than the one that made it,
because Meta's toasts have lied here before (2026-10-08: "A modositas nincs
mentve" arrived with a partial save).
- Contact (B1) was ALREADY SET, by the operator, before this run: Graph reads
emails ["info@felhom.eu"] and phone "+36702378499". Verified, not typed.
- Place (B2) already city-only Budapest, as the operator chose. Service area
REFUSED: Facebook offers the field but its picker has no "Magyarorszag",
only cities. Control: "Szeged" returns Szeged. Left unset rather than
narrowed to Budapest, which would shrink a coverage claim nobody authorised.
- Categories (B3) DONE: Informatikai vallalat (kept first, the only one shown)
+ Internetes ceg + Szoftverceg. Facebook's Hungarian list has no IT-support,
IT-consulting or cloud category; eleven terms searched, and the one true
match is a repair counter, which the fences rule out.
- Hours (B4) CANNOT be set and need not be: Facebook requires a street address
first, which the fences forbid. Measured on the rendered page with controls
present -- Zarva 0, Nyitva 0 while Budapest 1, Informatikai vallalat 1. The
Page will never show "Zarva".
- Messenger FAQ (B5) REFUSED -> R-920: the automation does not exist for this
Page. Catalogue holds exactly three templates; search "kerdes" returns none
while the control "uzenet" returns two. COPY.md section 5 is written anyway,
questions verbatim from gyik.html and answers condensed from each question's
own answer, and waits like section 2 does (R-917).
- Link preview correct in both languages, re-scraped once each; only the
expected fb:app_id warning, deliberately not fixed. Both report HTTP 206
where a plain curl gets 200 -- Facebook's scraper, preview complete.
fb_probe.py read now also reports emails, phone, category_list, location,
single_line_address and hours, ONE FIELD PER CALL: a batched fields= list fails
whole when any member is unreadable, which would let one refused field hide the
other five. Refusals are logged verbatim and never retried; "null" and "not
returned" are logged apart. hours is never returned by Graph, which is why B4's
read-back had to come from the rendered page.
Also corrects the "Business & legal" header, which read 12 rows (P2 5) over a
section holding 11 (P2 4); with R-920 it is 12 (P2 4, P3 1, P4 7). Only the
section this commit edits -- the other drifting headers belong to a session
that owns the register.
No post, no invite, no money, no app or portfolio change, website unchanged.
The app is no longer the unmeasured view. Solved against the laptop frame, your
Facebook Lite shot gives a visible window of x 349..1290 and your Chrome-mobile
shot x 349..1291 -- the LITE APP CROPS EXACTLY LIKE SIGNED-IN MOBILE WEB, and
both agree with the emulator's 351..1289. Their profile circle is at x 645..1021
from y 451, LOWER than the emulator's y 369, so that figure was pessimistic
rather than wrong and the margin it bought was real. Five views measured now;
the signed-out one is still the binding constraint.
Cover C follows your draft: the wordmark big on top (88 px, was 44), a small
gap, then the catchphrase. The catchphrase is ONE line, not the two you drew,
and the measurement forces it: with the signed-out circle starting at y 232, a
wordmark that size plus two catchphrase lines cannot both sit above it. Drawn as
two lines it measured 392 sampled text pixels behind that circle -- "saját
szabályaid" read "saját szab" there, which is the R-919 defect itself. Sizing
the two lines to fit instead drops the capital to 25 px, the legibility floor.
One line keeps the wordmark big, the capital at 34 px and every measured view
clean: 0 text pixels behind any circle. The laptop moved right and shrank to
540 px to free the width; 64% of it is cropped or covered somewhere, which the
build reports and which is what the decor layer is for.
You saw the cover uncropped signed out, after R-919 said the sides are cut.
Both are true; my first measurement was one view generalised to "the phone".
SIGNED IN, confirmed on your real phone (Chrome/Android, 1080 px): the crop is
real. The cover band is 708 px tall across 1080 = 1.525:1 against the file's
2.628:1, and solving the laptop frame's left edge against that scale puts the
window at x 351..1298 where the emulated Pixel 9 said 351..1289 -- the left edge
to the pixel. R-919 is confirmed on hardware, not replaced.
SIGNED OUT, measured from your screenshot: the box is 412x132 = 3.121:1, WIDER
than the file, so the height is cut, not the width -- and the file's blue top
rule is still visible at the top edge, which puts the cut at the BOTTOM: the top
525 px of 624 survives. The circle is far bigger and higher: x 486..1150 from
y 232, 41% of the width.
So the sides are cut for one visitor and the bottom for the other. build.py now
carries both views; SAFE is their intersection, (391,40)-(1249,485).
Two changes made that workable rather than merely safe:
- the circles are modelled as DISCS, not rectangles running to the bottom. Near
its top a disc is a few pixels wide; the rectangle was discarding most of the
lower cover for nothing, which is much of why the frame looked empty.
- TWO LAYERS. The READ layer (catchphrase, wordmark) must survive every view and
the check fails on it. The DECOR layer may be cropped or covered, and the build
REPORTS the cost instead of forbidding it (B 39%, C 62%). Before the split one
check governed both, so no laptop big enough to read could ever pass.
Red-proof again: the two-view geometry convicted all three covers as they stood
-- A 908 content px under a circle (its wordmark sat inside the signed-out
circle), B 1715 plus content past the cut bottom, C 1962. control_old_window
still convicts the pre-R-919 layout, so there are two controls now.
Covers redrawn: C is your laptop idea -- catchphrase and wordmark left, the
dashboard at 640 px (was 370) running off the right edge, readable at last. B's
home motif grew the same way. A lifted into the tighter band. Capitals 48/45/43
against the 25 px floor. Profile pictures untouched, not in the diff.
Still VERIFY: three views measured, all disagreeing with each other and with
Meta's help page, and the Facebook APP is still the one nobody has measured.
Operator refinements after looking at the rebuilt pictures. No geometry changed:
R-919's measured constants, its band and every check stand, and the red-proof
still convicts the old layout.
Profile picture: the logo MARK only. The mark plus the "felhom.eu" lettering was
too much for the 176 px Facebook shows -- and smaller than the shape the Page
carried before this work, because build.py shrinks the artwork to fit inside the
circle where the original was simply cropped by it. Dropping the lettering grows
the mark from 69% to 76% of the circle; canvas 932 -> 648 and the profile_width
floor 720 -> 640 (twice Meta's recommended 320 source; 720 was above what the
mark alone needs and would have shrunk it for nothing). logo.png is split at a
MEASURED row: ink y 5-289, blank band y 290-295, wordmark y 296-403.
Covers: the headline is now set the way the WEBSITE sets its h1. site.css
.page-index .hero-text h1 is font-weight 700, letter-spacing -0.03em; this file
used ExtraBold 800 with no tracking, so the cover did not actually match the
page. HEADLINE_WEIGHT/HEADLINE_TRACK now carry those, with real per-glyph
spacing. Bold is narrower so headlines grew: A 57->62, B 58->63, C 47->50.
"felhom.eu" is no longer typed anywhere: wordmark() draws the logo's own
lettering, which is set in "M+ 2c"/"Vremena Grotesk" -- fonts this machine does
not have (R-916) -- so any typed version was a look-alike. The website hero
shows the same logo.png on the same dark background, so the covers match the
page. The DOMAIN constant is removed rather than left as dead code.
Caught before it shipped: the preview's new profile paragraph added a fourth %d
and the argument tuple stayed in the old order, so the phone paragraph rendered
"the centre 76 px of the 938-pixel width, with a profile circle 1640
cover-pixels across". Every number was real, just in the wrong slot, which is
why it read as plausible instead of crashing. Found by reading the rendered
paragraph back, not by the exit code.
Built on DooPlex; all checks pass, the phone simulation still shows 0 px cut and
0 px under the profile circle on all three covers. R-919 stays VERIFY.
build.py believed a phone shows 640x360 of the cover. It shows 412x274 =
1.504:1 -- the full height and only the centre 938 px of 1640, 351 px off each
side. PHONE_HDR = (412, 274) now drives CW_PHONE, so SAFE is (391,40)-(1249,584),
858 px wide with a 40 px margin inside each crop edge.
QUIET is no longer one formula for both circles: the computer circle is
left-anchored, the phone circle is CENTRED and hides only its measured box
(637,369,393). The old shared formula, applied to a centred circle, would have
blanked everything from x 0 to x 1054 below y 345.
Every cover draws in a derived BAND (408,48)-(1249,340), and a cap check holds
each headline's capital at >= 4% of the cover height; A/B/C come out at 45, 45
and 37 px against a 25 px floor.
RED-PROOF, and it is permanent: control_old_window() runs on every build and
draws the headline where the old assumption put it (x 328); the check must
reject it. The phone's crop edge is x 351, so 23 px were cut; the measured safe
edge is x 391. Against the covers as committed at a76207945e the new check
convicted 3 of 3 -- A 23/22 px past the left/right edges, B 63/58 px plus 1017
content pixels under the phone circle, C 63/82 px plus 1778.
Covers redrawn inside the band: A one centred line; B the home motif scaled
with two tiles moved into the lower-right strip, the one area below the band a
phone still shows beside the profile circle; C the dashboard screen 470 -> 370
px so frame and base fit. preview.html shows the phone panel at the measured
938x624 with the centred circle and no longer claims 640x360.
Profile pictures untouched -- sha256 identical before and after, and not in the
diff. Built on DooPlex: the PNGs are byte-reproducible there (Pillow 11.1.0) and
not across Pillow versions; the README now says so.
R-919 is VERIFY, not closed: the geometry is measured on ONE emulated device in
the mobile website, so the operator's check in the Facebook app is the
acceptance. Nothing uploaded, no Graph API call, FACEBOOK_API never read.
Your suggestion to use DevTools device mode was right; "cannot be checked" was
too quick a conclusion. On an emulated Pixel 9 (412x924, mobile UA, with a
reload so Facebook serves the mobile bundle) the Page's mobile header renders
412x274 = 1.504:1. Facebook keeps the cover's full height and shows only the
centre 938 px of its 1640 px width -- 351 px off each side. build.py's safe
area leaves 306 px clear per side, 45 px too few, and the light text in
cover-c.png runs x333..x997 against a left crop edge of x351, so 18 px are lost:
the headline reads "aját szabályaid" and the wordmark "elhom.eu" on every phone.
Nothing re-cropped on Facebook (the task's fence). The fix is build.py's safe
area (<=938 px, ~900 for margin) plus the measured 172 px centred phone profile
circle, then a hand re-upload. R-919 opened; R-918 closed and moved to
CLOSED-ITEMS with the recipe that made the check possible.
Meta's help page is wrong about Meta's own rendering: it states 2.4:1 for the
mobile cover where the header measures 1.504:1, so this session's first-pass
arithmetic -- explicitly labelled arithmetic, not a check -- under-predicted the
crop about fivefold. That is recorded rather than quietly deleted.
Also: the evidence secret scan needed --exclude=README.md, because that README
quotes the search strings and was matching itself (4 false EAA hits, 1 false
access_token); with the exclusion, control 1 -> 0 and no real hit. And the
Business & legal section header was already miscounted before this session
(said 9/P2 4, actually held 10/P2 5) -- corrected; five other section headers
drift too and are named in the report, left for a session that owns the register.
The DooPlex gate run at 8664cbda is rc=0, all 18 gates OK; the Windows run's two
failures are platform artifacts (fcntl, symlink privilege, C:/E: mount, cp1250
console) and the deliberate workspace-CLAUDE.md divergence.
CI gates.yml #846 for 8664cbda78: Success, 4m39s. The Gitea API refused every
credential in the store (401), so the run was read from Gitea's web UI, which
serves the Actions list unauthenticated; /actions/runs/846 redirects to
/actions/runs/1573, R-417's index-vs-id offset again, so both numbers are on the
screenshot.
STATUS: the Facebook section rewritten for what is now live, the open-items
count corrected to the counted 140.
Ran marketing/facebook/TASK-page-setup-windows.md from the Windows workstation
through Claude in Chrome. Evidence in
documentation/audits/facebook-page-setup-2026-10-08/, report in
REPORT-facebook-page-setup.md.
Done, each read back from a different channel than the edit: the intro is
COPY.md §1 (Graph `about` hex-equal, 99 chars); the action button is
"További információ" to https://felhom.eu/; the Messenger welcome is COPY.md §3
(hex-equal) and the automation is on; the username is felhom.eu, so the Page
answers at facebook.com/felhom.eu.
Not done: COPY.md §2 has no field to go in — Facebook's current Pages
experience has no long-description field and the Graph `description` is null
and unwritable with the robot key's scopes. R-917, four options, operator's
call; no Hungarian copy was shortened.
Phase A read Meta's size page first-hand: it carries none of the 820x312 /
640x360 figures our README asserted, and its own two ratios contradict the file
it recommends. README's grade and comparison rewritten; what we build is
unchanged.
Phase C measured the computer width (cover whole, no crop, profile circle 16 px
clear). The 390 px phone check was NOT performed: narrowing a desktop browser
never gives Facebook's phone layout. R-918.
Nothing posted; no ad, no boost, no Meta Verified; the Meta app stays in
development mode; no host, guest, box or hub touched.
build.py (Pillow, site tokens + Plus Jakarta Sans) with in-build checks: profile circle (today's shape 6823 pixels
outside, new 0), cover safe area and profile-circle clearance, sizes read back. COPY.md: intro 99/100, long
description, Messenger welcome, three first posts, each claim traced to the website. R-914 gets the listing-based
removal proof and the picture-API tasks; R-915 the footer-link note; R-916 the logo's vector master.
Read phase passes (Page 1360018983863273, CREATE_CONTENT/MODERATE/ANALYZE, page token PAGE expires_at 0, three insights
metrics alive on v26.0). Write test: removal check accepts Meta's code-10 'Object does not exist' (fixed without a row,
4 tests); run 1 evidence kept. R-914 READY, R-915 narrowed to Live mode.
scripts/facebook/fb_probe.py (stdlib, read + write-test, no real-post command) with tests; read run twice:
SYSTEM_USER, expires_at 0, /me/accounts empty, so D/E did not run and nothing was posted. Findings, redacted
evidence, CONTEXT decision home, STATUS item, scripts CHANGELOG, REPORT-facebook-page-api.md.
Mealie, Homebox, Dawarich, Komga, Radicale, Recipe Importer (SVG) and SparkyFitness (PNG) are white now
(operator request), checked rendered in headless Chrome. crafty/grampsweb/homeassistant/plantit/uptimekuma
-> crafty-controller/gramps-web/home-assistant/plant-it/uptime-kuma, so the dashboard finds them (it asks
for <slug>-logo.*); both apps pages updated. R-912 filed (no gate checks this). Register 134 -> 135.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
Register 131 -> 134 (R-909 Apps card header, fixed on main; R-910 retake the website's dashboard pictures
after tomorrow's release; R-911 storage-unavailable banner stays Hungarian on English pages).
Evidence: before (0.303.0) / after (0.304.0-layout.rc2) pictures and measurements on scratch 9202, red proofs,
fixture diff. 9202 is back on 0.303.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb