New text/event-stream endpoints — /o/{offer_id}/events (buyer/seller only)
and /a/{auction_id}/events (public). Each polls the row ~every 1.5s, emits
a `data: {json}` frame on connect and whenever the state-machine state
changes, sends a heartbeat comment, then closes after ~25s so the browser
EventSource reconnects — "bounded" because uWSGI is sync (~16 worker
threads) and a long-lived SSE would starve the pool. Shared helper
lib/sse.py (sse_response / event_stream); it uses its own short-lived DB
session per poll (request.dbsession is already closed by pyramid_tm by the
time the streaming generator runs). Timings come from settings
(app.sse.hold_seconds / app.sse.poll_interval_seconds; test.ini sets them
tiny so the streaming tests finish in ~0.06s).
Client: auction.js opens the EventSource and feeds each frame into its
existing applyState(); it falls back to polling /a/{id}.json every 5s
where EventSource is unavailable. offer.js opens the EventSource on the
offer page and reload()s on a state change (the whole layout depends on
state / can_act). offer.j2 carries data-offer-state. Caddy auto-detects
text/event-stream and stops buffering — no Salt change.
Tests: 4 new functional tests (both endpoints stream the right
content-type + a state frame; 404 for outsiders / unknown ids). 994 passed.
- Offer history & offer page show the buyer's display name (User.display_name
= the public `name` handle; `full_name` is private) linked to a profile
page — never the email. _serialize_offer drops buyer_email; events carry
actor_name/actor_handle/actor_id, header carries buyer_name/buyer_handle.
- New public profile page: GET /profile/{handle} (views/user.py:user_profile,
template profile.j2). Shows gravatar (User.gravatar_url(size) — forced
identicon unless the user opted into Gravatar), member-since, owned/edited
shops, and a <details> "Show email" that is server-gated: only the user
themselves, or a shop owner/editor viewing in that shop's context
(?shop={shop_id}) when the profile user has transacted there (an offer or
an invoice).
- New operator offers inbox: GET /s/{shop_id}/offers (@shop_editor_required,
shop_offers.j2) — open offers first, each row links to /o/{id} and the
buyer's profile. Reachable from /actions/view via a new "Offers" button
(shown when shop.offer_enabled).
- /actions/view rebuilt: one flat .action-button-grid (Grid auto-fit,
minmax(15rem,1fr)) inside a properly-padded .action-columns well — fixes
the off-balance two-column layout and buttons overflowing the well; no
<br> spacers. Styleguide gains profile-card and action-button-grid
patterns.
- offer.j2: buyer name shown (linked to profile); "Buyer:"/"Seller:" message
lines renamed "Buyer note:"/"Seller note:" to disambiguate.
Tests: 11 new functional tests (profile render + email gating, offers inbox,
actions button, styleguide). 989 passed.
- offer.j2 renders a state-aware notice (.offer-state-notice, .alert
variants) above the action forms: declined / withdrawn / expired /
accepted (+ pay-now hint for buyer) / your-turn / waiting — so the
viewer always understands the offer state without depending on a flash
a JS redirect would skip. _serialize_offer now exposes is_declined,
is_withdrawn, is_expired, is_pending, is_countered, is_accepted.
- Shop-settings Make-an-Offer section restyled with the new
.settings-form / .settings-form-grid / .settings-field /
.settings-field-hint system (two-up grid, per-field hints, submit
pinned right). Added a styleguide entry under #forms.
- Reworded the section blurb: auto-declined offers are NOT silent — the
buyer is told their offer was too low; only the seller isn't pinged.
- Fixed --color-text-muted typo (→ --text-muted) on .offer-js-flash-info.
Tests: TestOfferRoutes gains 3 state-notice render tests;
TestSettingsFormStyleguide covers the styleguide + live shop-settings
markup and asserts the old wording is gone. 978 passed.
Every bid/buy-now/watch and offer open/counter/accept/decline/withdraw
POST now works as a plain browser submit: flash + 302 redirect to the
auction/offer page. JSON is returned only when the request carries
X-Requested-With: XMLHttpRequest. Adds offer.js progressive-enhancement
layer (mirrors auction.js); pay-now CTA on accepted offers; .offer-js-flash
styling; grid layout for offer/action forms. offer_accept emails the
buyer only on the transition into ACCEPTED.
Tests: TestOfferRoutes/TestAuctionRoutes now drive the JSON path via an
AJAX helper; new TestOfferNoJsFallback/TestAuctionNoJsFallback cover the
plain-POST redirect path. 973 passed.