SeekquarrySeekquarry - Yioop Repo - Seekquarry

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Phase 7 &mdash; Wiki UX &mdash; Arc Plan</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px;
    margin: 1.5em auto; padding: 0 1em; line-height: 1.5; color: #1a1a1a; }
h1 { font-size: 1.4em; }
h2 { font-size: 1.1em; margin-top: 1.4em; }
ol { margin: 0.3em 0; } li { margin: 0.25em 0; }
ol.items { list-style: none; padding-left: 0; }
ol.items.numbered { list-style: decimal; padding-left: 2em; }
.check { color: #1a7f37; font-weight: bold; }
.next { color: #b35900; font-weight: bold; }
.review { color: #0969da; font-weight: bold; }
.nope { color: #82071e; font-weight: bold; }
.q { color: #8250df; font-weight: bold; }
code { background: #f2f2f2; padding: 0 0.25em; border-radius: 3px; }
em { color: #555; }
</style>
</head>
<body>
<h1>Phase 7 &mdash; Wiki UX</h1>
<p><em>Arc started 2026-07-22, after Phase 6. Sections are Roman numerals
and steps within them are lettered; each step's title says what it does,
followed by a sentence on what was proposed and, once worked, a few on what
was done and anything learned. Marker legend:
<span class="next">&#9654;</span> being worked on,
<span class="review">&#9873;</span> implemented and waiting to be reviewed,
<span class="check">&#10003;</span> done,
<span class="check">&#10003;</span><span class="nope">&#10007;</span>
closed by deciding not to do it, <span class="q">?</span> open decision,
unmarked = planned.</em></p>

<h2>What this arc is for</h2>
<p>The wiki is where a group keeps its shared pages, so its editing and
resource surfaces are what a group touches most. This arc finishes the
page-resource folder UI (I), gives the editor a live preview and then a
markup toolbar (II), makes the category mechanism a real content tool (III),
completes the email witness flow for secret ballots (IV), and documents the
folder UI once it is finished (V).</p>

<h2>I. Finish the page-resource folder UI</h2>
<p>To-do item 17. Most of it landed in Phase 2.5; two pieces stayed open.</p>
<ol class="items">
<li><span class="check">&#10003;</span> A. <strong>Multi-select with keyboard
    shortcuts.</strong> Pick rows with click, Shift-click and Control-click,
    then act on the picked set with Delete, F2, Control C/X/V and Escape.
    <p>Rows are picked in the browser and nothing is sent until an action is
    asked for, when the whole set goes as one request. That needed the
    server widened: <code>SocialComponent</code> now answers
    <code>delete_resources</code>, <code>move_resources</code>,
    <code>clip_copy_resources</code> and <code>clip_cut_resources</code>,
    each taking a list. Dragging carries the picked set to either drop
    target, dimming the rows it carries and showing a count.</p></li>
<li><span class="check">&#10003;</span> B. <strong>CSS-Grid
    auto-fill rewrite of the folder layout.</strong>
    Replace the fixed-<code>in</code>-width table with
    <code>repeat(auto-fill, minmax(min, 1fr))</code> and a container query so
    the folder reflows to the width it is given.
    <p><code>table-layout: fixed</code> and four hard column widths gave the
    list a 502px floor that overflowed a phone's folder box by 156px; the
    list is now a grid whose rows take the table's tracks through
    <code>subgrid</code>, and the cards are <code>auto-fill</code> so they
    need no column count at all. <code>.wiki-resources</code> is the query
    container, so narrowing the folder with the window left wide is what
    drops columns, which the old viewport media query could not express.
    Measured: list content 502px to 344px at a 360px window, cards 167px to
    282px in a 576px folder, desktop unchanged.</p></li>
</ol>

<h2>II. Editor preview and assist</h2>
<p>To-do items 19 and 20. The renderer in both is the existing
<code>WikiParser</code>, so neither adds a client-side parser.</p>
<ol class="items">
<li><span class="review">&#9873;</span> A. <strong>Live preview
    pane.</strong> Split the editor into textarea and preview, refreshed as
    the source is typed by rendering it through <code>WikiParser</code>.
    <p>The rendering happens in the browser through the port of
    <code>WikiParser</code> already in <code>help.js</code>, so a keystroke
    costs no request; it waits for a pause in typing, and nothing is
    rendered at all until the book beside the way into the page's history
    opens the pane, which starts closed and leaves the source the whole
    width until it does; the book is shut while the preview is out of
    sight and open while it is being read. Source and preview share one window of a settled
    height, split by the existing <code>split-adjuster</code> from
    <code>wiki.js</code>, and the editor's row of controls moves above both
    columns as the preview opens so the two start at the same place and end
    level; either can be given more room by dragging, each is put at the other's place in the document as it is
    scrolled. Under 52rem the pair becomes one column with the preview
    above the source, and a markdown group gets no pane since only the
    mediawiki engine is ported. Picking out words in either pane shows them
    in the other: the two are matched on their letters and digits alone,
    since markup is in one and not the other, and the answer is carried
    back to where those letters sit in each. A resource written out as its
    own data draws in the preview, since everything needed is in the
    markup.</p></li>
<li>B. <strong>Markup-insertion toolbar.</strong> Buttons that insert wiki
    markup at the cursor, with the textarea still the source of truth and the
    preview from II.A showing the result. Comes after II.A. Proposed.</li>
</ol>

<h2>III. Category templates</h2>
<ol class="items">
<li>A. <strong>Flesh out the category mechanism.</strong> Grow
    <code>{{category|...}}</code> and <code>{{category-list|...}}</code> into
    a real content tool: named parameters, style presets, pagination,
    auto-stub index pages, a "Filed under" affordance, cross-group lookup,
    and semantic HTML in place of the custom tags. To-do item 18.
    Proposed.</li>
</ol>

<h2>IV. Asynchronous secret-ballot witnesses</h2>
<ol class="items">
<li>A. <strong>Email-based witness flow.</strong> Complete the asynchronous
    flow for <code>{{secret-ballot|w1|w2|...}}</code> so named witnesses take
    part by email rather than having to be present at once. To-do item 21.
    Proposed.</li>
</ol>

<h2>V. Document the resource-folder UI</h2>
<ol class="items">
<li>A. <strong>Write up the wiki resource-folder UI.</strong> Document the
    folder interface and its actions as wiki help source delivered as a
    <code>.txt</code>, not edited into the autogenerated help pages. Best
    done after I. To-do item 37. Proposed.</li>
</ol>

<h2>Order and dependencies</h2>
<p>I, II, III and IV are independent. Within II the preview comes before the
toolbar; V waits on I so it describes finished behavior.</p>

<h2>Other work accomplished</h2>
<p>Oldest first.</p>
<ol class="items numbered">
<li><span class="check">&#10003;</span> <strong>Git clone and pull over
    HTTP/2 failed.</strong> The Phase 6 smart-HTTP serving built the whole
    packfile synchronously on the event loop, so the server answered nothing
    while it ran and the client's connection went unserviced.
    <p>Git requests now run on the cooperative fiber path and the build
    suspends periodically. The earlier attempt to pace the send with
    <code>stream()</code> was aimed at the wrong layer and, by dropping
    <code>webExit</code>, let the group controller render onto the reply; it
    was undone here.</p></li>
<li><span class="check">&#10003;</span> <strong>Pull failed with "bad line
    length character: PACK".</strong> A client with a long history negotiates
    in rounds, sending haves without saying "done" and expecting only an
    acknowledgment; the server answered with the packfile.
    <p>A round with no depth and no "done" now returns just the
    acknowledgment, and skips the history walk since that round asks for no
    objects. A five-commit client sends every have and "done" at once, which
    is why the trouble looked size-dependent.</p></li>
<li><span class="check">&#10003;</span> <strong>Negotiation never settled and
    packfiles were held in memory.</strong> The server never named a commit
    the two sides shared, so a client gave up after a couple of hundred
    offers and asked for the whole history, which was then built as one
    string.
    <p>The server now offers <code>multi_ack_detailed</code>, naming
    recognized commits as common and closing the round with
    <code>NAK</code>; a flush there is read as a missing acknowledgment and
    no closing line leaves the client waiting. The reply is written to a
    stream with a running checksum. A seven-hundred-commit pull settles in
    two rounds, and 17.7MB is built with 4.0MB of memory against 21.7MB as a
    string.</p></li>
<li><span class="check">&#10003;</span> <strong>A deferred reply cannot also
    stream.</strong> <code>completeDeferredResponse</code> sends only the
    buffered body, so a git request on the fiber path sent headers claiming
    eighty megabytes and then nothing.
    <p><code>GitUploadPack</code> now yields its reply piece by piece and
    <code>serveUploadPack</code> hands that to the ordinary streaming path.
    The fiber breathing and its interval constant went with it.</p></li>
<li><span class="check">&#10003;</span> <strong>An HTTP/2 reply must state its
    length.</strong> Sending with no stated length gave
    <code>content-length: 0</code> and an empty body, since the length is
    taken from the buffered body, which a streamed reply leaves empty.
    <p>The yielded pieces are written to a temporary file, its size is the
    stated length, and the file is streamed back and removed. Building before
    sending means a very large clone occupies the server while its packfile
    is made.</p></li>
<li><span class="check">&#10003;</span> <strong>Every version of every file
    was sent.</strong> The <code>have</code> lines stopped the commit walk but
    nothing was set against what the client holds, so a one-commit update
    packed a copy of the whole working tree.
    <p>The walk now gathers everything the client's known commits record and
    skips it. A client one commit behind was sent 63 objects and 17.6MB and
    is now sent 3 objects and 1,755 bytes.</p></li>
<li><span class="check">&#10003;</span> <strong>Objects were sent
    whole.</strong> A new <code>GitDelta</code> writes and reads Git's delta
    format, working on bytes so binary content is handled too.
    <p><code>Utility.php</code>'s <code>diff</code> and
    <code>computeLCS</code> were considered and not used: they compare lines
    and build a table capped at a quarter of a million cells.
    <code>GitPackReader</code>'s private applier moved into
    <code>GitDelta</code>, leaving one implementation. A clone of a
    forty-commit history of a large file fell from 898,559 to 105,915
    bytes.</p></li>
<li><span class="check">&#10003;</span> <strong>Differences could only be
    built against objects in the pack.</strong> An update to a file whose
    earlier version the client already held was still sent whole.
    <p>The server now offers <code>thin-pack</code> and remembers the version
    of each file the client holds, sending a new version as its difference
    from that. A one-line edit fell from 22,508 bytes to 331.</p></li>
<li><span class="check">&#10003;</span> <strong>A deep clone of the live
    repository stopped partway.</strong> The reply stated 34,671,195 bytes
    and the connection closed with a differing amount delivered each time,
    over HTTP/1.1 as well as HTTP/2.
    <p>Not the link: the same server sent a 142MB file from the repository to
    the same machine complete, so what differs is how the two are sent, and
    the packfile now goes out through <code>readfile</code> as that file
    does. The earlier change to the idle sweep, which treats a connection
    with a reply still to send as active, stays, though it was never tied to
    this. <code>H3Listener</code> does not share that defect, its idle check
    counting outbound packets.</p></li>
<li><span class="check">&#10003;</span> <strong>Say where the server's memory
    is going.</strong> The web server sat at 1924MB of a 2000MB limit for
    minutes, so the search that finally failed asked for 1.25MB and had
    nowhere to put it.
    <p>The memory line now also reports about how many bytes the kept
    sessions hold, from a sample of twenty, and how many sit in connection
    buffers. Sessions are capped by count rather than weight, so their number
    alone did not say.</p></li>
<li><span class="check">&#10003;</span> <strong>The mail server ran out of
    memory during a read.</strong> It sat at about 120MB for an hour, then
    went to 445MB and to its 1GB limit within ten seconds, with the
    connection buffers empty at the last reading before it died.
    <p>A connection's unread input was allowed 256MB on a 1GB limit, half of
    memory while the buffer grows, so it is now 32MB, one message plus its
    command. A stored message was read whole however large the file had
    become, so its size is checked first, on the storage every backend
    shares. The loop now records which IMAP command it is running, so the
    next crash line names it.</p></li>
<li><span class="check">&#10003;</span> <strong>A missing wiki page left nulls
    running through the media path.</strong> A warning, a deprecation and a
    second warning on one request were one defect:
    <code>getPageInfoByName</code> answers <code>false</code>, reading
    <code>PAGE</code> off it warns and yields null, and that null reached
    <code>preg_match</code>.
    <p>All three now check that what came back is an array, in both places
    the media path does this, and the sign-in check treats a missing page as
    an empty one.</p></li>
<li><span class="check">&#10003;</span> <strong>Every page was wider
    than its own window between about 601 and 1060 pixels.</strong>
    <code>min-width: 66rem</code> on <code>.body-container</code> meant the
    content column could never be narrower than 1056px however narrow the
    window was, and the phone layout does not take over until 600px.
    <p>That width and the floor under it are now held to the window as well
    as to their design, and seven more floors underneath doing the same
    thing give way to their container the same way. Measured on two pages at
    ten widths, the document's scroll width now equals the window at every
    width from 601 up, where it had been stuck at 828 and 933. It fed back
    into I.B, where the folder can now take widths that left one stretched
    card per row.</p></li>
<li><span class="check">&#10003;</span> <strong>Reporting an issue against a
    repository needed an account.</strong> The tracker is public to read, so
    a visitor who hits a bug either registers or says nothing, and letting
    anyone report must not also let anyone spam.
    <ol class="items">
    <li><span class="check">&#10003;</span> a. Let a signed-out visitor
        submit a report, held rather than numbered, since an issue number is
        permanent and a spam run would spend a block of them. The reporter
        gives an unverified handle stored beside an opaque token derived
        from their address, so an editor can recognize a repeat reporter
        without seeing one.
        <p>A submission passes a backoff counted apart from the sign-in
        forms, a decoy field, a timing check, stateless proof of work and a
        rolling daily allowance, and is told in six words or fewer what
        became of it. Testing found the proof of work replaced the string an
        answer was answering before reading the answer, and kept it in a
        session a cookieless visitor does not have, so it now takes string
        and time back from the form. A refusal names its reason except where
        naming it would tell a script what to fix.</p></li>
    <li><span class="check">&#10003;</span> b. Add the editor queue as one
        more issue filter listing waiting reports, minting the number
        through the existing <code>createGitIssue</code> only on acceptance.
        An accepted issue shows the handle where the reporter column would
        otherwise show a username.
        <p>Rows are selected the way rows are selected elsewhere and decided
        together, and rejecting runs through one control whose catch also
        shuts the reporter out for a week, a month or for good, with a
        searchable paged list of who is shut out and a control to let them
        back in. Both lists page through one query narrowing on page name
        and record text, so only the rows shown are opened, and each list
        is read by scrolling: reaching the end asks for the next page and
        the rows arrive into the table, the way a feed reads. A waiting
        report and a ban are each a page beside the repository, told apart
        from a numbered issue by the mark after its name, which needed a
        one-off rename of existing issue pages.</p></li>
    <li><span class="check">&#10003;</span> c. Give the repository a setting
        &mdash; none, moderated or unmoderated &mdash; defaulting to
        moderated, so it can be on for one repository and off elsewhere.
        <p>It is a field in the repository page's own settings rather than a
        group setting, which keeps the whole feature clear of any schema
        change. <code>validateHashCode</code> moved from
        <code>RegisterController</code> up to <code>Controller</code> so the
        account forms and this one ask the same question through one copy of
        it.</p></li>
    </ol></li>
<li><span class="review">&#9873;</span> <strong>A second column heading
    appeared part way down a scrolled issue list.</strong> The pager took
    every row out of an answer, so when an answer was a whole page rather
    than the rows alone, the page's own heading row came with it while the
    markup around it was dropped by the table parser.
    <p>It now takes only rows of the list itself, and an answer carrying
    none of them ends the paging rather than being asked for again.
    Measured against a whole-page answer, the old reading gave 11 rows with
    a heading among them and the new one gives 10 rows with none. The rows
    are also read back inside a table rather than a detached table body,
    which is the one context where a browser is required to parse them as
    rows.</p></li>
<li><span class="review">&#9873;</span> <strong>An activity that is mostly
    working area was held to a reading width.</strong> The content column
    stopped well short of the browser window and the activity inside it was
    capped narrower still, which left the wiki editor, mail and messages
    using a fraction of a wide screen.
    <p>The column now stands 1.2rem narrower than the window, through a
    named property, the margins either side of the centered container are
    halved to 1.5rem, and an activity whose width is working room rather
    than a line of prose carries a class that drops the cap: the wiki page
    whether written or read, mail, and messages. Measured at 1920 pixels
    the column went from 1322 to 1901 and the wiki page from 885 to 1853 in
    both modes, with nothing overflowing at 360, 601, 800 or 1060 pixels.
    Every length this arc added is said in rem and every direction as a
    logical property, which is what Phase 5 settled the stylesheet on.</p>
    </li>
<li><span class="review">&#9873;</span> <strong>The mail server ran out
    of memory again, during an IMAP command.</strong> It climbed from 114MB
    to its 1GB limit in about two minutes with the connection buffers empty
    and no fibers parked, and the fatal named only "imap key=2259 UID",
    which says neither whose mail was being read nor whether the command
    was a fetch, a search or a store.
    <p>The crash line now names the account, the folder and the word after
    UID, and the periodic memory line says how many folder indexes are
    cached and how many records they hold, so the next occurrence answers
    both questions itself. Rebuilding a folder's index no longer grows its
    snapshot as one string nor reads the whole index a second time as
    records; it streams both files a line at a time and takes only flags
    from the index. Measured on a folder of 77,000 messages that is 2.0MB
    held beside the records against 6.2MB, but the same measurement shows
    neither figure explains a climb of 319MB, so the cause is not yet
    established.</p></li>
<li><span class="review">&#9873;</span> <strong>The server's default
    settings were stated in two places and the constructor set only two of
    them.</strong> The full set was built inside <code>listen</code>, so
    anything reading a setting before listening began found nothing there,
    and the root index carried its own copy of the request-size default.
    <p>The settings now live in <code>defaultServerGlobals</code> alone.
    The constructor starts a server with all of them rather than two,
    <code>listen</code> starts from the same call, and the root index asks
    for the one it wants. Checked against the source, all fifteen settings
    read anywhere in the class are among them, bar one read with a fallback
    of its own.</p>
    <p>Separately, the file cache remembers the hash of what was last read
    from or written to each path, and a write whose contents match that
    hash is skipped, so saving something unchanged costs no disk
    write.</p></li>
<li><span class="review">&#9873;</span> <strong>A session's contents were
    held in memory with nothing bounding their size.</strong> Ten thousand
    sessions were allowed whatever they each weighed, and the reported
    figure was an average of twenty scaled up, which would miss one large
    session among many small ones entirely.
    <p>A session's contents now live in a file of their own, read back when
    a request needs them and written through the cache above, so a request
    that does not change a session costs no write at all. What the server
    holds for each is its time, its use-count and nothing else, which is
    all the sweep for stale sessions and the eviction of the least recently
    used ever needed; evicting one now takes its file with it. A session
    grown past what the server accepts as a request body is refused rather
    than written.</p>
    <p>The block in <code>listen</code> that restored sessions from a
    configuration array is gone: nothing produced what it read.</p></li>
<li><span class="review">&#9873;</span> <strong>A test run printed a line
    that was not a test result.</strong> Refusing to read an oversize
    stored message went to the error log, which under the command-line PHP
    is standard error, so the run said so in the middle of its results.
    <p>A storage class now reports a problem through a handler that can be
    handed in, falling back to the error log when none is. The test that
    makes an oversize message catches it, so a run says only what the tests
    themselves say, and the saying of it is checked rather than merely
    heard. Measured on a filtered run, standard error is empty and every
    line on standard output is a test result or a heading.</p></li>
<li><span class="review">&#9873;</span> <strong>The cached folder indexes
    were bounded by how many folders were held, not by how much.</strong>
    One mailbox of a hundred thousand messages outweighs sixty small ones,
    and the rebuild that compaction calls for rewrote the whole index every
    time even when it produced what the file already held.
    <p>The cache now has a ceiling on the message records held across it
    and drops the least recently used until they fit, and a folder too
    large to fit on its own is not held at all rather than emptying the
    cache for something that still would not. The snapshot a rebuild would
    write is hashed a line at a time and the write is skipped when the file
    already holds it. Measured on an index of 154,853 lines and 77,427
    messages, the file is 3.2MB, one read costs 0.352 seconds and the
    records it makes cost 48MB.</p>
    <p>What the cache holds changed too: three lists of plain values beside
    each other, keyed by uid, rather than a small array for every message.
    A record holds four scalars and PHP spends more on the array around
    them than on the values inside, so the same folder costs 6.01MB where
    it cost 48MB, and building the records back out of it takes 0.035
    seconds against 0.352 to parse the index file again. A first attempt at
    this packed the values into one binary string, which measured slower to
    decode than re-reading the file and was dropped; plain values keyed by
    uid need no unpacking at all. Counting live messages needs no records
    built, since the uids are the keys the compact form is already keyed
    by.</p></li>
</ol>

</body>
</html>