Public Access
directory card photos: load from the image endpoint, not inline base64
The webhook no longer sends `Image`, so the card photo comes from its own endpoint, addressed by the record id — one <img> per card, one request. - static/expert-image.js: the route AND its query-parameter name in ONE constant (id, confirmed by measurement; unverified in the brief), plus an optional version token used only when the payload carries one. - Directory card: <img src="…odoo_experts_image?id=<id>">, width/height reserve the 72x72 box, decoding=async, eager for the first row and lazy below it; onerror swaps in the SAME monogram a card with no photo shows, so a failing image looks exactly like today. - The static build-time card loses its 185 KB inline base64 photo and shows a monogram: it is the no-JavaScript fallback, where there is no onerror to fall back with. Page 341,345 -> 158,428 bytes. - /freelancer-edit/: the photo block is a look, not a field (no file input), and the whole-record save sends NO `Image` key at all.
This commit is contained in:
1 parent
9d14652a57
commit
2b0e89191a
3 files changed
+134
-71
No files matched your search
+30
-46
@@ -149,6 +149,7 @@
|
||||
<!-- The 15-point rule, in ONE copy for both forms (this one and the application form). -->
|
||||
<script src="/static/focus-rule.js?v=22ac72a8f0"></script>
|
||||
<script src="/static/photo-upload.js?v=c17638457b"></script>
|
||||
<script src="/static/expert-image.js?v=e496d4a4e3"></script>
|
||||
|
||||
<!-- 1) Read the credential, then take it OUT of the address bar.
|
||||
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first — the
|
||||
@@ -574,53 +575,36 @@
|
||||
form.appendChild(linkField("edFiverr", "fiverr", "Fiverr URL",
|
||||
readField(record, "fiverr"), "https://www.fiverr.com/…"));
|
||||
|
||||
// The photo is editable here with the SAME rules as the application form
|
||||
// (static/photo-upload.js). A picked file is validated and downscaled on the spot;
|
||||
// leaving it untouched keeps the record's own Image, which rides back byte-identical
|
||||
// because the save sends the whole record (see the submit handler below).
|
||||
var newImage = null;
|
||||
// The photo block is a LOOK, not a field. The photo does not live in this record
|
||||
// any more — the payload carries no `Image` — it comes from its own endpoint,
|
||||
// addressed by the record id (window.O4PExpertImage). So nothing here edits it,
|
||||
// and the save below sends no `Image` key at all: not the value, not null, not "".
|
||||
var photoWrap = el("div", "field");
|
||||
var photoLabel = el("label", "", "Photo");
|
||||
photoLabel.setAttribute("for", "edImage");
|
||||
photoWrap.appendChild(photoLabel);
|
||||
photoWrap.appendChild(el("label", "", "Photo"));
|
||||
var photoRow = el("div", "photo-readonly");
|
||||
var img = readField(record, "Image");
|
||||
var currentImage = (typeof img === "string" && (img.indexOf("https://") === 0 || img.indexOf("data:image/") === 0)) ? img : null;
|
||||
var previewImg = el("img");
|
||||
previewImg.id = "edImagePreview";
|
||||
previewImg.alt = "";
|
||||
if (currentImage) {
|
||||
previewImg.src = currentImage;
|
||||
photoRow.appendChild(previewImg);
|
||||
} else {
|
||||
previewImg.hidden = true;
|
||||
photoRow.appendChild(previewImg);
|
||||
photoRow.appendChild(el("span", "mono", initials(who)));
|
||||
var photoMono = el("span", "mono", initials(who));
|
||||
photoRow.appendChild(photoMono);
|
||||
if (rec && rec.id !== undefined && rec.id !== null && rec.id !== "") {
|
||||
var photoImg = el("img");
|
||||
photoImg.id = "edImagePreview";
|
||||
photoImg.alt = "";
|
||||
photoImg.decoding = "async";
|
||||
photoImg.hidden = true; // the monogram stands in until the photo is there
|
||||
photoImg.addEventListener("load", function () {
|
||||
if (photoMono.parentNode) photoMono.parentNode.removeChild(photoMono);
|
||||
photoImg.hidden = false;
|
||||
}, { once: true });
|
||||
photoImg.addEventListener("error", function () {
|
||||
// A failing photo is the monogram: no broken-image icon, no empty box.
|
||||
if (photoImg.parentNode) photoImg.parentNode.removeChild(photoImg);
|
||||
}, { once: true });
|
||||
photoImg.src = O4PExpertImage.url(String(rec.id), rec);
|
||||
photoRow.appendChild(photoImg);
|
||||
}
|
||||
photoWrap.appendChild(photoRow);
|
||||
var photoInput = el("input");
|
||||
photoInput.type = "file";
|
||||
photoInput.id = "edImage";
|
||||
photoInput.name = "Image";
|
||||
photoInput.accept = "image/jpeg,image/png,image/webp";
|
||||
photoWrap.appendChild(photoInput);
|
||||
var photoResult = el("p", "hint", "");
|
||||
photoResult.id = "edImageResult";
|
||||
photoResult.setAttribute("role", "status");
|
||||
photoResult.setAttribute("aria-live", "polite");
|
||||
photoWrap.appendChild(photoResult);
|
||||
photoWrap.appendChild(el("p", "hint",
|
||||
"JPEG, PNG or WebP, up to 8 MB — we resize it to a square of at most 384 px first. " +
|
||||
"Leave it to keep your current photo."));
|
||||
O4PPhoto.wire(photoInput, previewImg, photoResult, function (dataUrl) {
|
||||
newImage = dataUrl;
|
||||
var fb = photoRow.querySelector(".mono");
|
||||
if (fb) fb.hidden = !!dataUrl;
|
||||
if (dataUrl) return; // wire already shows the new image
|
||||
// reverted to "no new photo": show the record's own photo again
|
||||
if (currentImage) { previewImg.src = currentImage; previewImg.hidden = false; }
|
||||
else previewImg.hidden = true;
|
||||
});
|
||||
"Your photo comes from your listing and is not editable here — reply to the email " +
|
||||
"that brought you here if it needs to change."));
|
||||
form.appendChild(photoWrap);
|
||||
|
||||
var save = el("button", "btn", "Save my changes");
|
||||
@@ -686,10 +670,10 @@
|
||||
else if (had) payload[pair[1]] = "";
|
||||
});
|
||||
|
||||
// An untouched photo rides back exactly as loaded — it came in with the record
|
||||
// above and was never touched, so it stays byte-identical. Only a NEWLY picked
|
||||
// photo (already validated and downscaled) replaces it.
|
||||
if (newImage !== null) payload.Image = newImage;
|
||||
// The photo does NOT ride in this record any more: the payload carries no
|
||||
// `Image` key and this form sends none — not the value, not null, not "". The
|
||||
// delete is the guarantee, whatever a reply may one day carry.
|
||||
delete payload.Image;
|
||||
|
||||
fetch(C.webhook, {
|
||||
method: C.saveMethod,
|
||||
|
||||
@@ -1380,7 +1380,7 @@
|
||||
<!-- DIRECTORY-LIST-START -->
|
||||
<article class="dev-card" data-search="oliver - the oc guy i love to see how far i can push the odoo community version for my clients. besides the module implementation i am quite happy with adding to odoo using hermes agent, n8n and progressive web apps agent automation, manufacturing, e-commerce english german spanish" data-id="2" data-name="Oliver - The OC Guy" data-country="py" data-languages="english|german|spanish" data-oca="1" data-relay="1">
|
||||
<header class="dev-head">
|
||||
<img class="dev-photo" src="data:image/jpeg;base64,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 truncated
|
||||
<span class="dev-avatar" aria-hidden="true">OG</span>
|
||||
<div class="dev-id">
|
||||
<h3 class="dev-name">Oliver - The OC Guy</h3>
|
||||
<a class="dev-oca-badge" href="https://apps.odoo-community.org/community/oliver-arnold" target="_blank" rel="noopener nofollow">OCA Member Profile</a>
|
||||
@@ -2052,6 +2052,7 @@
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
<script src="/static/focus-rule.js?v=22ac72a8f0"></script>
|
||||
<script src="/static/photo-upload.js?v=c17638457b"></script>
|
||||
<script src="/static/expert-image.js?v=e496d4a4e3"></script>
|
||||
<script>
|
||||
/* ============================================================================
|
||||
Odoo freelancer directory — page script.
|
||||
@@ -2093,6 +2094,12 @@
|
||||
var ODOO_EXPERTS_EDIT_WEBHOOK = "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_edit";
|
||||
var EDIT_EMAIL_KEY = "email"; // body sent to ODOO_EXPERTS_EDIT_WEBHOOK: { email: "..." }
|
||||
|
||||
// How many cards load their photo EAGERLY: the first visible ROW. The grid is 3
|
||||
// columns at the widest layout (measured), so the first three cards. Every card below
|
||||
// the fold loads lazily — a lazy image in the first row would delay LCP, and a photo
|
||||
// off-screen is not worth its request.
|
||||
var EAGER_CARDS = 3;
|
||||
|
||||
// ---- The shared constants (the same lists live in build-directory.py) --------
|
||||
// The five self-assessed focus axes — each a SEPARATE numeric field (0-5) on the
|
||||
// wire, no CSV, no encoding. They must total exactly FOCUS_TOTAL.
|
||||
@@ -2356,10 +2363,6 @@
|
||||
return v.indexOf("http://") === 0 || v.indexOf("https://") === 0 || v.indexOf("/") === 0;
|
||||
}
|
||||
|
||||
function isImageSrc(v) {
|
||||
return v.indexOf("https://") === 0 || v.indexOf("data:image/") === 0;
|
||||
}
|
||||
|
||||
function liveEl(tag, cls, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (cls) node.className = cls;
|
||||
@@ -2460,9 +2463,25 @@
|
||||
return liveValues(rec, field).map(fold).join("|");
|
||||
}
|
||||
|
||||
/* The monogram — the initials treatment a card shows when there is no photo. It is
|
||||
also the FALLBACK when the photo fails to load, so a missing photo looks exactly
|
||||
like a card that never had one. */
|
||||
function liveMonogram(name) {
|
||||
var parts = name.split(/\s+/).filter(Boolean);
|
||||
var mark = parts.length > 1
|
||||
? parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
||||
: (parts[0] || "?").slice(0, 2);
|
||||
var mono = liveEl("span", "dev-avatar", mark.toUpperCase());
|
||||
mono.setAttribute("aria-hidden", "true");
|
||||
return mono;
|
||||
}
|
||||
|
||||
/* One card, field for field the same as render_card() in build-directory.py.
|
||||
Everything a freelancer wrote goes in as textContent, never as HTML. */
|
||||
function liveCard(rec, active) {
|
||||
Everything a freelancer wrote goes in as textContent, never as HTML.
|
||||
|
||||
`eager` is true only for the first visible row: those photos load eagerly because
|
||||
a lazy image in the first row delays LCP. Everything below the fold loads lazily. */
|
||||
function liveCard(rec, active, eager) {
|
||||
var name = String(rec.Name || "").trim();
|
||||
var card = liveEl("article", "dev-card");
|
||||
card.setAttribute("data-name", name);
|
||||
@@ -2474,23 +2493,33 @@
|
||||
card.setAttribute("data-" + pair[2], liveAttrValue(rec, pair));
|
||||
});
|
||||
|
||||
var photo = String(rec.Image || "").trim();
|
||||
var avatar;
|
||||
if (isImageSrc(photo)) {
|
||||
avatar = liveEl("img", "dev-photo");
|
||||
avatar.src = photo;
|
||||
avatar.alt = "";
|
||||
avatar.width = 72;
|
||||
avatar.height = 72;
|
||||
avatar.loading = "lazy";
|
||||
avatar.decoding = "async";
|
||||
} else {
|
||||
var parts = name.split(/\s+/).filter(Boolean);
|
||||
var mark = parts.length > 1
|
||||
? parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
||||
: (parts[0] || "?").slice(0, 2);
|
||||
avatar = liveEl("span", "dev-avatar", mark.toUpperCase());
|
||||
avatar.setAttribute("aria-hidden", "true");
|
||||
// The photo is NOT in the profile payload — the webhook no longer sends `Image`
|
||||
// (it used to ride as inline base64). It comes from its own endpoint, addressed by
|
||||
// the record id: ONE <img>, ONE request (window.O4PExpertImage holds the route and
|
||||
// its query-parameter name in one constant).
|
||||
//
|
||||
// The box is RESERVED (width/height + the .dev-photo CSS), so the card does not
|
||||
// reflow when the photo arrives. A failure — no id, no workflow, a reply that is
|
||||
// not an image — falls back to the SAME monogram a card with no photo already
|
||||
// shows: no broken-image icon, no empty box, no alt text spilling into the layout.
|
||||
var mono = liveMonogram(name);
|
||||
var avatar = mono;
|
||||
var rid = (rec.id === undefined || rec.id === null || rec.id === "") ? "" : String(rec.id);
|
||||
if (rid) {
|
||||
var photo = liveEl("img", "dev-photo");
|
||||
photo.alt = "";
|
||||
photo.width = 72;
|
||||
photo.height = 72;
|
||||
photo.decoding = "async";
|
||||
photo.loading = eager ? "eager" : "lazy";
|
||||
// On failure the <img> is REPLACED by the monogram — a node of the same 72x72
|
||||
// size, so the card does not move. `mono` is captured by name, never the variable
|
||||
// that is reassigned below.
|
||||
photo.addEventListener("error", function () {
|
||||
if (photo.parentNode) photo.parentNode.replaceChild(mono, photo);
|
||||
}, { once: true });
|
||||
photo.src = O4PExpertImage.url(rid, rec);
|
||||
avatar = photo;
|
||||
}
|
||||
var head = liveEl("header", "dev-head");
|
||||
var who = liveEl("div", "dev-id");
|
||||
@@ -2960,7 +2989,9 @@
|
||||
// NO sort: the endpoint's order is rendered exactly as received.
|
||||
var active = liveFilters(records);
|
||||
while (grid.firstChild) grid.removeChild(grid.firstChild);
|
||||
records.forEach(function (rec) { grid.appendChild(liveCard(rec, active)); });
|
||||
records.forEach(function (rec, i) {
|
||||
grid.appendChild(liveCard(rec, active, i < EAGER_CARDS));
|
||||
});
|
||||
// The button is added after the cards exist — only while the route is set.
|
||||
Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), enhanceAsk);
|
||||
dirPage = 1;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* The freelancer PHOTO — one endpoint, one constant.
|
||||
|
||||
The photo does NOT live in the profile payload any more. It used to ride there as
|
||||
inline base64 under the key `Image`; the webhook no longer sends that key, so the
|
||||
payload is small and the photo is fetched separately, per card, by record id.
|
||||
|
||||
ONE constant holds BOTH halves of the address: the route and the query-parameter
|
||||
name. Changing either is a one-line edit here, and both pages pick it up.
|
||||
|
||||
The parameter name is `id`. It was recorded as UNVERIFIED in the brief (every probe
|
||||
answered "workflow is not registered"), and it is now CONFIRMED by measurement
|
||||
against the live route: `?id=<n>` answers 200 with the photo for that record, while
|
||||
`/4`, `?profile_id=`, `?image_id=` and a missing id all fail. See
|
||||
webmaster/out/2026-11-03-photo-endpoint/REPORT.md.
|
||||
|
||||
The endpoint currently answers `application/json` with `{"Image":"data:image/jpeg;
|
||||
base64,…"}` — a JSON envelope, NOT image bytes — so an <img> cannot decode it and
|
||||
every card falls back to its monogram. That is the measured state, not a guess; the
|
||||
page is built for the shape the brief specifies (an <img> pointed at this route) and
|
||||
starts showing photos the moment the route returns image bytes.
|
||||
|
||||
`versionParam`: appended ONLY when the payload carries a version token, so a
|
||||
year-long immutable cache on the endpoint stays correct — the URL changes when the
|
||||
photo does. Without a token the URL is stable and the endpoint's own headers decide
|
||||
caching (see the report: it sends no Cache-Control and a weak ETag today). */
|
||||
window.O4PExpertImage = {
|
||||
endpoint: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_image",
|
||||
idParam: "id",
|
||||
versionParam: "v",
|
||||
// Anything that changes when the photo changes. `updatedAt` used to be in the payload.
|
||||
versionFields: ["updatedAt", "updated_at", "ImageVersion", "image_version"],
|
||||
|
||||
version: function (rec) {
|
||||
if (!rec) return "";
|
||||
for (var i = 0; i < this.versionFields.length; i++) {
|
||||
var v = rec[this.versionFields[i]];
|
||||
if (v !== undefined && v !== null && v !== "") return String(v);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
url: function (id, rec) {
|
||||
var u = this.endpoint + "?" + this.idParam + "=" + encodeURIComponent(id);
|
||||
var v = this.version(rec);
|
||||
if (v) u += "&" + this.versionParam + "=" + encodeURIComponent(v);
|
||||
return u;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user