Commit Graph
7 Commits
Author SHA1 Message Date
webmaster bd0881f882 cache-bust local assets; narrow the language facet to scores >= 3
Part 1 — asset versioning: every local <script src> and <link rel=stylesheet> on the
tracked pages now carries ?v=<sha256-of-the-file> (10 hex), computed at BUILD time from
the file's bytes by work/build-asset-versions.py — never hand-typed, and idempotent
(re-running with no file change rewrites nothing). This is what makes an edited
focus-rule.js / photo-upload.js a new URL instead of a stale cache hit.

Part 2 — language facet: a language now counts for the Language filter only when its
0-5 self-assessment is >= LANG_FACET_MIN (3 = 'works in it'). The constant sits beside
the 15-point rule in the page script and its mirror in build-directory.py. Nothing
stored changes; the card's languages chart still prints every language above 0, the
search box is unchanged, and the facet stays an exact membership test against the
card's value. A '1' language no longer offers the person under that language.
2026-11-03 21:26:58 +00:00
webmaster cdf166632a feat(freelancer-edit): add the missing GitHub link field
The edit form carried LinkedIn/Upwork/Fiverr but not GitHub, though the
application form, the webhook and the card renderer already do. Add a
fourth optional link field mirroring the other three, under the live wire
key verbatim (lower-case github; never normalised), pre-filled from the
record and added to the save loop with the same rules: a key is written
only when the input has a value, an empty optional field never invents ""
on a record that never had one, and a link the owner cleared sends "".
No new URL check — the card's isLink() stays the only url predicate.
2026-11-03 20:48:31 +00:00
webmaster 8319a74f99 feat(freelancer): validate and downscale photo uploads on both forms
Both the application form (/odoo-community-developers/) and the self-service
edit form (/freelancer-edit/) now use one shared module, static/photo-upload.js
(window.O4PPhoto), so the limits live in one place:

- validate() decides on the File object BEFORE any read: type allowlist
  (image/jpeg, image/png, image/webp) and an 8 MiB cap. A refused file is read
  zero bytes and is never submitted.
- downscale() reads, centre-crops square, caps the longest side at 384 px,
  encodes JPEG at 0.82 and steps the quality down until the result is <= 80 KiB.
  Re-encoding also strips EXIF (a phone photo's GPS does not reach the page) and
  refuses a spoofed type that fails to decode.
- The edit form's photo field becomes a real labelled input that keeps the
  current photo when untouched; the whole-record save carries it back
  byte-identical.

Existing stored photos are untouched, so the directory page is not lighter until
profiles re-upload. accept= is a hint, not validation.
2026-11-03 19:07:07 +00:00
webmaster 5cf58ffa8a feat(freelancer-edit): add LinkedIn/Upwork/Fiverr fields; save the whole record
Two defects, one file. (1) The edit form was missing the three profile links the
application form collects. Added as optional type=url fields pre-filled from the
record, with the application form's own labels and placeholders, under the LIVE
wire keys verbatim: "Linkedin" (capital L), "upwork", "fiverr" — casing never
normalised. (2) The save built its payload field-by-field, so Image and the three
links were absent from the body and a freelancer saving an edit wiped their own
photo and links. The payload now starts from the loaded record (read-modify-write)
and overwrites only the fields this form owns; Image rides through untouched. An
empty optional link on a record that never had one is left as the record's own
value — no invented empty strings. 'has' stays first, method stays PATCH.

No second URL check: the only url predicate is the directory card's isLink().
2026-11-03 18:51:51 +00:00
webmaster 7dfd5aec9b fix(freelancer-edit): save sends the hash and uses PATCH
The save was authorised by the record id alone — ids are small
sequential integers, publicly visible on every directory card, so
a PATCH carrying only an id let one visitor overwrite another's
listing. The save now:

- uses PATCH (C.saveMethod), matching n8n
- carries the hash in the body under the same "has" key the
  delete already sends, so save and delete share one credential
  path, alongside the record id
- refuses to fire at all when the page holds no hash, and says why
  in the status line

Everything else is unchanged: the 15-point gate, receipt only on
2xx, the honest failure line that keeps the edits, the save.disabled
handling and the DELETE shape.
2026-11-03 18:40:29 +00:00
webmaster 768dc1151a fix(freelancer): the 15-point rule is now one shared constant, in both forms
The edit form's legend read "Skill areas" and carried no directive, while the
application form read "Skill profile — 15 points to distribute" and said what to
do. Same rule, worded in two places — and the edit form lost the sentence.

- New static/focus-rule.js exposes window.O4PFocus: LEGEND, DIRECTIVE, TOTAL, MAX
  and message(sum). Both forms render the legend, the directive and the
  wrong-total message from it, so the two cannot drift apart again.
- Edit form: legend from the constant, the application form's directive under it,
  and the blocked-save message now names the direction.
- Application form: legend set from the constant, live warn and sum gate use the
  same message(); the all-zero gate keeps the directive.
- Message wording: over -> "remove N", under -> "add N", zero -> "place all 15
  points"; the total, aria-live, data-bad, the blocked save and the
  fieldset/legend structure are unchanged.

The rule text now appears once: "You have 17 of 15 — remove 2."
2026-11-03 18:34:43 +00:00
webmaster 1a1b62136f feat(freelancer): card cog + self-service modal, and the private freelancer-edit page
Card (odoo-community-developers/):
- "Ask question anonymously" moves into its own row ABOVE the links row, in the
  runtime renderer and in build-directory.py (the build-time template).
- A cog icon button sits next to it: a real <button> with an accessible name
  (aria-label="Edit or delete your profile"), same quiet weight as its neighbour.
- Pressing the cog opens a modal that explains the flow and asks for the email
  address, then POSTs {"email":"..."} to odoo_experts_edit (body shape in one
  constant, EDIT_EMAIL_KEY). The reply is never read: one message on any 2xx, so
  the box cannot tell a visitor whether an address is on file. A non-2xx fails
  honestly and shows no receipt. Both controls ship dark (added only while their
  route constant is set), so neither is ever a dead control.

New private page (freelancer-edit/, noindex+nofollow, unlinked, absent from
sitemap.xml and llms.txt):
- Emailed link is https://odoo4projects.com/freelancer-edit/?has=<hash> — the
  same shape as /freelancer-answer/; reads `has` first, falls back to `hash`.
- GETs the edit webhook with the hash and pre-fills the form: Name, Focus (<=70),
  description (<=230), the five axis numbers (must total 15), the three language
  numbers (0-5), Country, OCA_link. The GET reply is read through one alias map;
  a shape it does not recognise names the fields it received instead of guessing.
  404 "is not registered" is a CONFIGURATION error, never "your link is invalid";
  404/500 "No item to return was found" is an invalid/expired link; a network
  failure keeps the hash for a retry.
- Save = POST with the record id (one constant, SAVE_METHOD). Delete is a
  deliberate second action (a confirm tick gates the button) and never reports
  success on a non-2xx.
- The image is read-only here, stated in the UI: replacing a photo is a different
  problem from editing text.
- The hash never reaches analytics: the query is stripped before the Plausible
  snippet loads. referrer policy no-referrer; JS off says so plainly.
2026-11-03 17:59:41 +00:00