OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / 1ab8af1

0.0.48: a banner field in settings
The daemon can hold a banner as of favela-peer 0.0.49; this is where a person sets one. Two pictures, one form, and they are independent: each is only touched when its own part carries bytes, so saving with just a name typed does not clear either picture. The preview shows the banner at its own width rather than in the avatar's circle — they are different shapes and previewing one as the other teaches the wrong thing. The multipart cap goes 2 MiB -> 3 MiB because the form can now carry two 512 KiB pictures. The slack is still there for the same stated reason: an oversized image should be refused by the daemon with a message about its size, not by the transport with nothing useful. /avatar/<oid> already serves any oid from the same store, so the banner needed no new route. 175/175. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1ab8af1aa01dced24c6a76fe39f91f171f411c5b
by plankatron <fleeky@gmail.com> · 2026-09-09T22:49:31.000Z

Changed files (3) +24 -8

lib/pages.js +11 -0
……@@ -251,6 +251,7 @@ button.liked{font-weight:700;border-color:var(--accent);color:var(--accent)}
251251
252252/* The composer is not the page. It was a third of the first screen before any content: a tall box,
253253 a permanent file input and two lines of help that never changed. */
254.banner-lg{width:100%;max-height:190px;object-fit:cover;border-radius:10px;border:1px solid var(--line)}
254255.composer{background:var(--bg-soft);border:1px solid var(--line);border-radius:12px;
255256 padding:var(--s2);margin-bottom:var(--s4)}
256257.composer textarea{border:0;padding:var(--s1);min-height:3.2rem;resize:vertical}
……@@ -1433,6 +1434,11 @@ export function settings (profile, message, handle = null, namespace = null, tok
14331434 const shown = p.avatar
14341435 ? '<p><img class="av-lg" src="/avatar/' + esc(p.avatar) + '" alt="your avatar"></p>'
14351436 : '<p class="who">No picture yet.</p>'
1437 // The banner is shown at its own width rather than in the avatar's circle — they are different
1438 // pictures with different shapes, and previewing one as the other teaches the wrong thing.
1439 const shownBanner = p.banner
1440 ? '<p><img class="banner-lg" src="/avatar/' + esc(p.banner) + '" alt="your banner"></p>'
1441 : ''
14361442 return layout('settings', note + reachSettings(reach) +
14371443 '<h1>Settings</h1>' + shown +
14381444 '<form method="post" action="/settings" enctype="multipart/form-data">' +
……@@ -1442,6 +1448,11 @@ export function settings (profile, message, handle = null, namespace = null, tok
14421448 '</label></p>' +
14431449 '<p class="who">png, jpeg, gif or webp, up to 512 KiB. The type is read from the file itself, ' +
14441450 'not its name.</p>' +
1451 shownBanner +
1452 '<p><label>Banner<br><input type="file" name="banner" accept="image/png,image/jpeg,image/gif,image/webp">' +
1453 '</label></p>' +
1454 '<p class="who">The wide header image. Same formats and cap. Leaving either file empty leaves ' +
1455 'that picture as it was.</p>' +
14451456 // ⛔ The profile record has had a body slot from the start and nothing ever wrote it, so every
14461457 // screen that wanted to say who somebody IS had a name, a picture and 64 hex characters.
14471458 '<p><label>About you<br><textarea name="bio" rows="3" placeholder="a sentence or two — markdown">' +
lib/router.js +11 -6
……@@ -757,11 +757,12 @@ export async function route (req, res, ctx) {
757757 }
758758
759759 if (req.method === 'POST' && path === '/settings') {
760 // 2 MiB: the avatar cap is 512 KiB, and the slack is so an oversized picture is refused by the
761 // daemon with a message about its size rather than by the transport with nothing useful.
760 // 3 MiB: each picture is capped at 512 KiB by the daemon and the form can now carry TWO, and
761 // the slack is so an oversized picture is refused by the daemon with a message about its size
762 // rather than by the transport with nothing useful.
762763 let form
763764 try {
764 form = parseMultipart(await readBytes(req, 2 * 1024 * 1024), req.headers['content-type'])
765 form = parseMultipart(await readBytes(req, 3 * 1024 * 1024), req.headers['content-type'])
765766 } catch (err) {
766767 return seeOther(res, '/settings?m=' + encodeURIComponent(err.message))
767768 }
……@@ -772,13 +773,17 @@ export async function route (req, res, ctx) {
772773 if (typeof form.fields.name === 'string') set.name = form.fields.name
773774 if (typeof form.fields.bio === 'string') set.bio = form.fields.bio
774775 if (Object.keys(set).length) await client.call('set-profile', set)
775 const file = form.files.avatar
776 if (file && file.bytes.length) {
776 // ⛔ TWO PICTURES, one form. The avatar is the round one; the banner is the wide header.
777 // They are independent — submitting the form with only one file set must not clear the other,
778 // which is why each is only touched when its own part carries bytes.
779 for (const [field, verb] of [['avatar', 'avatar-put'], ['banner', 'banner-put']]) {
780 const file = form.files[field]
781 if (!file || !file.bytes.length) continue
777782 // ⛔ A PATH, for the same reason. Written where the daemon can read it; it copies what it
778783 // keeps, so this temp file is ours to remove.
779784 const tmp = join(os.tmpdir(), 'favela-upload-' + Date.now() + '-' + Math.random().toString(16).slice(2))
780785 fs.writeFileSync(tmp, file.bytes)
781 try { await client.call('avatar-put', { path: tmp }) } finally { try { fs.unlinkSync(tmp) } catch {} }
786 try { await client.call(verb, { path: tmp }) } finally { try { fs.unlinkSync(tmp) } catch {} }
782787 }
783788 } catch (err) {
784789 return seeOther(res, '/settings?m=' + encodeURIComponent(err.message))
package.json +2 -2
……@@ -1,7 +1,7 @@
11{
22 "name": "favela-surface",
3 "version": "0.0.47",
4 "description": "The favela renderer — server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
3 "version": "0.0.48",
4 "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
55 "type": "module",
66 "main": "index.js",
77 "bin": {