Release history
Everything that has shipped, what changed and why. 128 releases listed.
v9.8.1
The two lines become one
Development had split off v9.6.7 into two lines that never saw each other: one
built OHSTV, the Media Center and the Discord provider (its own v9.6.8 through
v9.7.4), the other fixed cam connections and made Sparks configurable (its own
v9.6.8 through v9.7.1). Both were real, neither was wrong, and they used the
SAME VERSION NUMBERS for different work - which is the part that had to be
cleaned up rather than papered over.
THE MERGE ITSELF WAS CLEAN, which is worth stating because it was checked
rather than hoped. Against the common v9.6.7 ancestor the two lines touched
exactly one line of index.php in common: the version string. Every other change
sat in a different region of the file. Nothing was dropped and nothing was
reconciled by judgement:
- from the OHSTV line: the Media Center and Free TV Streams work, the Discord
provider and its naming pass, the mesh-ladder guards that stop a switched-off
camera being treated as a wedge, the SFU receiver cleanup for peers who left,
and the changelog audit
- from the cam line: the cam_join carry that stops a rejoin un-publishing you,
every Sparks limit and heat weight becoming an admin setting, the VARIABLES
guide's session-lock and carry rules, and the sabotage gate
VERIFIED BOTH WAYS, not just assembled. The cam line's carry block is
byte-identical to the version that was tested; the OHSTV line's Discord and
Media Center strings are present at the same counts as in its own build. The
sabotage gate was re-run on the merged tree - 15 mechanisms across 6 suites, all
still provably failable - and the OHSTV line's own changelog audit was run
against it too, so each branch's gate had to accept the other branch's code.
ONE RULE THE MERGE HAD TO ENFORCE ACROSS THE SEAM. The cam line added a
requirement that every outbound HTTP call releases the PHP session lock first,
or it blocks that member's own heartbeat and room poll for the length of the
call. The OHSTV line was written without knowing that rule existed. Checked on
the merged file: 16 outbound call sites, none unguarded - the OHSTV work adds no
server-side fetches of its own.
The duplicated numbers are recorded honestly below: where both lines shipped a
v9.6.8, a v9.7.0 and a v9.7.1, that heading now carries both accounts with a
note saying why there are two.
v9.7.4
Discord is named where the room can see it
A SOURCE PEOPLE ACTUALLY USE SHOULD BE VISIBLE AS ONE. Discord went in as a
working provider in 9.7.3 but was still folded into "& more" everywhere a
member actually looks, which is the same as not shipping it - nobody pastes a
link they have no reason to think will work. It is named now in all four
member-facing lists: the hint above the chat box, the Media Center's empty
state, the empty-queue line, and the paste box's own placeholder, beside
YouTube, Twitch, Vimeo, SoundCloud and Spotify. It also sits with the named
services in the admin list rather than down among the generic direct-file and
.m3u8 rows.
v9.7.3
Discord uploads play in the Media Center
PASTE A DISCORD CLIP AND THE ROOM WATCHES IT. Video and audio uploaded to
Discord and shared with Copy Link now resolve like any other source: they queue,
they play for everyone, and because they are real files rather than an embed
they report their own ending, so the queue auto-advances the same way YouTube
and direct files do. Discord is a provider in its own right, which means it
appears with the rest under Admin -> Rooms -> Media Center and can be switched
off site-wide like any other service.
THEY WERE NOT SIMPLY FALLING THROUGH TO THE DIRECT-FILE HANDLER, for two
reasons worth writing down. Discord keeps the uploader's own filename in the
URL, and people's filenames have brackets in them - "clip (2).mp4" is the most
ordinary thing a person can upload - and the direct-file allowlist has no
parentheses, so those links bounced off it with "must be a plain https link",
which is a baffling thing to read about a link that plainly is one.
And since 2024 a Discord attachment URL only works while carrying the ex/is/hm
parameters Discord signs it with, where `ex` is the expiry as a hex timestamp.
A link with the query stripped off is a guaranteed 404 and an old one is a 404
with a date on it. Both are now said out loud at the point of pasting - expired,
or missing its signature, with the fix in the same sentence - instead of being
queued for the room to watch fail.
Worth knowing, because it is Discord's rule and not something this end can fix:
those signed links stay valid for roughly a day. A Discord item that has sat in
a room's history since last week will have expired at Discord's end, and needs
a fresh Copy Link.
v9.7.2
The changelog checks itself now
SEVEN RELEASES HAD SHIPPED WITH NO ENTRY IN THIS FILE. Nothing was wrong with
any of them - v7.6.9, v8.7.8, v9.4.1, v9.4.2, v9.5.1, v9.5.2 and v9.5.3 were
written and simply never written UP. They are documented now, recovered from the
source rather than from memory: every change in index.php is tagged with the
version that made it, so the file itself remembers what each release did even
when the changelog forgot.
AND THE ONLY THING THAT HAD BEEN PREVENTING THAT WAS SOMEBODY REMEMBERING, which
is not a process. A deploy now audits its own paperwork: it reads the version
markers out of the shipped source, compares them against the headings in this
changelog, and reports anything that ships code but has no entry. It runs beside
the package rebuild on every version change, so the answer is already waiting on
Admin -> Build Packages, and it is wrapped so a failed audit can never take a
page down - the same rule 9.5.1 applied to the builder.
It deliberately does NOT write entries for you. A machine can tell you a release
is undocumented; it cannot know what the release DID, and a generated stub in
front of a paying customer is worse than an honest gap.
v9.7.1
Cams: the mesh ladder stops fighting people who are fine + The gate
on the gates
NOTE: this number was used twice. Development ran on two lines, both
branched from v9.6.7: one on OHSTV and the Media Center, the other on
cam connections and Sparks. They were merged in v9.8.1, and both
accounts are kept here rather than one being dropped.
### Cams: the mesh ladder stops fighting people who are fine
TURNING YOUR CAMERA OFF MADE EVERYONE ELSE REBUILD YOUR CONNECTION. Voice Only
and the Bandwidth panel's audio preset disable the video track while the
connection stays up and perfectly healthy, so decoded frames stop dead and read
exactly like a wedge. The SFU watchdog has known that since v9.1.4. The mesh one
never did - so a camera switched off on purpose had every viewer restart ICE on
that peer, then tear the whole connection down and rebuild it, on a ~16s loop,
for as long as the camera stayed off.
PAUSING YOUR OWN RECEIVING DID IT TO THE WHOLE ROOM AT ONCE. The Bandwidth
panel's receive-pause deliberately stops this browser decoding remote video, so
every tile's frame count flatlines together. The setting that exists to SAVE a
user bandwidth was quietly ICE-restarting and then rebuilding every mesh peer
they had. Both guards now sit in front of the mesh ladder, where the SFU has had
them all along.
THE FREE RUNG, ON MESH TOO. Packets still arriving with no frame coming out is
the browser's decoder stuck, not the transport, and an ICE restart cannot fix a
decoder - it only costs the room a renegotiation to find that out. Re-attaching
the video element's source rebuilds the decode pipeline for nothing: no
renegotiation, no ICE restart, no tile teardown. Two per peer, and three seconds
of real video buys the budget back - the same terms the SFU rung uses.
AND THE MESH LADDER IS NOW IN THE ROOM LOG. Every decision it made was a
console.warn while every SFU event went to the cam log, so following one mesh
failure meant reading a panel and a browser console side by side. That is how a
mesh problem stays invisible while an SFU problem gets traced.
A PEER WHO LEFT IS NOT A PEER WHO FROZE. The SFU freeze watchdog walks our own
receivers, not the roster, and nothing removed an SFU-only peer's receiver when
they left: the mesh departure sweep only walks mesh connections, forgetPeer ran
only on a changed session id, and sync()'s purge stops running once the room
falls below the SFU threshold - which is exactly what happens the moment the
other person in a two-cam room closes. Their receiver stayed behind with its
frames stopped, and the ladder duly reported "frozen at N decoded frames ...
nothing arriving" and released tracks against somebody who had already gone.
Every one of those was a false alarm, and each fed the chronic counter for a
peer who was not there to be chronic. The watchdog checks the roster first now
and forgets them silently, trusting only a roster the poll actually returned.
ONE MORE FALSE ALARM, ON THE HEARTBEAT. "re-advertised our SFU session - the
presence row had lost it" could not be true on a current build: since v9.4.1 the
server repairs a lost advertisement inline, on the beat that carries it. The
answer only ever came back "gone" for a beat that carried NO advertisement -
which is every beat sent before start() has finished waiting for the connection
and announcing. If that finished while the beat was still in flight, the answer
described a peer that no longer existed, and we re-filed a session the real
announce had put there milliseconds earlier. It spent a locked write on the
busiest presence file at the busiest moment and burned the 10s rate limit, so a
genuine loss inside that window found the backstop already used up. It acts only
on a beat that actually carried an advertisement now.
### The gate on the gates
No behaviour changes. This release exists because 9.7.0 shipped with three
tests that had passed on broken builds, and "I remembered to check" is not a
process.
THE PROBLEM. A test that cannot fail is worse than no test: it reports safety
it never verified. Three did exactly that, each caught by a human happening to
sabotage that mechanism by hand:
- the headers-sent check printed 64 bytes and believed headers were sent.
Output buffering meant they were not, so it passed with the fix removed.
- the Sparks page-size check counted ".sp-card", which the composer and the
Daily Spark box also carry, so it returned the same number at every page
size and passed on a lever it had never exercised.
- the cam-effect carry check posted the wrong field name, stored nothing,
and then "the effect survived" passed comparing empty with empty.
THE FIX IS MECHANICAL. tests/sabotage.py breaks each shipped mechanism in turn
on a throwaway copy, runs the suite that claims to cover it, and requires NAMED
assertions to go red. A suite that stays green on a broken build now fails the
build. Fifteen mechanisms across six suites, all proven failable.
It guards against two ways of fooling itself. A break whose anchor no longer
matches after a refactor would silently apply nothing and the suite would
correctly pass - so every break asserts its anchor matches exactly once, and a
stale anchor is a loud failure rather than a quiet pass. And a suite broken in
some unrelated way would be red for every sabotage and prove nothing - so each
suite must first come back GREEN on the real build before any of its sabotages
count.
WHAT IT FOUND IMMEDIATELY, all in the sabotage mapping rather than the code,
which is the point - it refused to let the mapping be assumed:
- two breaks were UNOBSERVABLE on the tested path. sp_feed_hot's page-size
default is never reached from the page, because the renderer resolves the
limit and passes it down; and swapping only the condition on the cam_join
carry left the body reading a variable that is null on a reload, so the
result was unchanged. Both read as "vacuous gate" and were neither. The
runner now says which of the two it cannot distinguish, instead of
asserting the wrong one.
- one break turned its suite red at a different assertion than the one that
claimed to cover it - a real mis-attribution, now corrected.
AND ONE GENUINELY WEAK GATE, FIXED. The rejoin check ran a background watcher
sampling the presence file every 20ms while threads hammered the room, and
asserted it never saw a row without an advertisement. Whether it passed
depended on whether a sample landed inside the loss window; on a loaded machine
it could miss every one and pass with the fix removed. The loss happens AT the
rejoin, so it is checked there now: rejoin, read the row, assert. Twelve
consecutive rejoins, then twelve more under concurrent room traffic. No
threads, no sampling, no luck.
A HARNESS BUG WORTH RECORDING, because it made every result wander. The runner
served the extracted tree DIRECTLY for its first pass, so each suite wrote
registered users, rooms and rate-limit state into the very thing under test.
The first run passed, the tree was then dirty, and later runs failed
registration and produced different answers each time - which briefly looked
like a regression in the release. Every run now happens on a throwaway copy.
Nothing under test is ever written to.
Also: the session-lock probe took its source path from a hardcoded location, so
pointing it at a deliberately broken copy would have quietly tested the good
build and reported green. It takes the tree as a parameter now.
NOTE: this number was used twice. Development ran on two lines, both
branched from v9.6.7: one on OHSTV and the Media Center, the other on
cam connections and Sparks. They were merged in v9.8.1, and both
accounts are kept here rather than one being dropped.
### Cams: the mesh ladder stops fighting people who are fine
TURNING YOUR CAMERA OFF MADE EVERYONE ELSE REBUILD YOUR CONNECTION. Voice Only
and the Bandwidth panel's audio preset disable the video track while the
connection stays up and perfectly healthy, so decoded frames stop dead and read
exactly like a wedge. The SFU watchdog has known that since v9.1.4. The mesh one
never did - so a camera switched off on purpose had every viewer restart ICE on
that peer, then tear the whole connection down and rebuild it, on a ~16s loop,
for as long as the camera stayed off.
PAUSING YOUR OWN RECEIVING DID IT TO THE WHOLE ROOM AT ONCE. The Bandwidth
panel's receive-pause deliberately stops this browser decoding remote video, so
every tile's frame count flatlines together. The setting that exists to SAVE a
user bandwidth was quietly ICE-restarting and then rebuilding every mesh peer
they had. Both guards now sit in front of the mesh ladder, where the SFU has had
them all along.
THE FREE RUNG, ON MESH TOO. Packets still arriving with no frame coming out is
the browser's decoder stuck, not the transport, and an ICE restart cannot fix a
decoder - it only costs the room a renegotiation to find that out. Re-attaching
the video element's source rebuilds the decode pipeline for nothing: no
renegotiation, no ICE restart, no tile teardown. Two per peer, and three seconds
of real video buys the budget back - the same terms the SFU rung uses.
AND THE MESH LADDER IS NOW IN THE ROOM LOG. Every decision it made was a
console.warn while every SFU event went to the cam log, so following one mesh
failure meant reading a panel and a browser console side by side. That is how a
mesh problem stays invisible while an SFU problem gets traced.
A PEER WHO LEFT IS NOT A PEER WHO FROZE. The SFU freeze watchdog walks our own
receivers, not the roster, and nothing removed an SFU-only peer's receiver when
they left: the mesh departure sweep only walks mesh connections, forgetPeer ran
only on a changed session id, and sync()'s purge stops running once the room
falls below the SFU threshold - which is exactly what happens the moment the
other person in a two-cam room closes. Their receiver stayed behind with its
frames stopped, and the ladder duly reported "frozen at N decoded frames ...
nothing arriving" and released tracks against somebody who had already gone.
Every one of those was a false alarm, and each fed the chronic counter for a
peer who was not there to be chronic. The watchdog checks the roster first now
and forgets them silently, trusting only a roster the poll actually returned.
ONE MORE FALSE ALARM, ON THE HEARTBEAT. "re-advertised our SFU session - the
presence row had lost it" could not be true on a current build: since v9.4.1 the
server repairs a lost advertisement inline, on the beat that carries it. The
answer only ever came back "gone" for a beat that carried NO advertisement -
which is every beat sent before start() has finished waiting for the connection
and announcing. If that finished while the beat was still in flight, the answer
described a peer that no longer existed, and we re-filed a session the real
announce had put there milliseconds earlier. It spent a locked write on the
busiest presence file at the busiest moment and burned the 10s rate limit, so a
genuine loss inside that window found the backstop already used up. It acts only
on a beat that actually carried an advertisement now.
### The gate on the gates
No behaviour changes. This release exists because 9.7.0 shipped with three
tests that had passed on broken builds, and "I remembered to check" is not a
process.
THE PROBLEM. A test that cannot fail is worse than no test: it reports safety
it never verified. Three did exactly that, each caught by a human happening to
sabotage that mechanism by hand:
- the headers-sent check printed 64 bytes and believed headers were sent.
Output buffering meant they were not, so it passed with the fix removed.
- the Sparks page-size check counted ".sp-card", which the composer and the
Daily Spark box also carry, so it returned the same number at every page
size and passed on a lever it had never exercised.
- the cam-effect carry check posted the wrong field name, stored nothing,
and then "the effect survived" passed comparing empty with empty.
THE FIX IS MECHANICAL. tests/sabotage.py breaks each shipped mechanism in turn
on a throwaway copy, runs the suite that claims to cover it, and requires NAMED
assertions to go red. A suite that stays green on a broken build now fails the
build. Fifteen mechanisms across six suites, all proven failable.
It guards against two ways of fooling itself. A break whose anchor no longer
matches after a refactor would silently apply nothing and the suite would
correctly pass - so every break asserts its anchor matches exactly once, and a
stale anchor is a loud failure rather than a quiet pass. And a suite broken in
some unrelated way would be red for every sabotage and prove nothing - so each
suite must first come back GREEN on the real build before any of its sabotages
count.
WHAT IT FOUND IMMEDIATELY, all in the sabotage mapping rather than the code,
which is the point - it refused to let the mapping be assumed:
- two breaks were UNOBSERVABLE on the tested path. sp_feed_hot's page-size
default is never reached from the page, because the renderer resolves the
limit and passes it down; and swapping only the condition on the cam_join
carry left the body reading a variable that is null on a reload, so the
result was unchanged. Both read as "vacuous gate" and were neither. The
runner now says which of the two it cannot distinguish, instead of
asserting the wrong one.
- one break turned its suite red at a different assertion than the one that
claimed to cover it - a real mis-attribution, now corrected.
AND ONE GENUINELY WEAK GATE, FIXED. The rejoin check ran a background watcher
sampling the presence file every 20ms while threads hammered the room, and
asserted it never saw a row without an advertisement. Whether it passed
depended on whether a sample landed inside the loss window; on a loaded machine
it could miss every one and pass with the fix removed. The loss happens AT the
rejoin, so it is checked there now: rejoin, read the row, assert. Twelve
consecutive rejoins, then twelve more under concurrent room traffic. No
threads, no sampling, no luck.
A HARNESS BUG WORTH RECORDING, because it made every result wander. The runner
served the extracted tree DIRECTLY for its first pass, so each suite wrote
registered users, rooms and rate-limit state into the very thing under test.
The first run passed, the tree was then dirty, and later runs failed
registration and produced different answers each time - which briefly looked
like a regression in the release. Every run now happens on a throwaway copy.
Nothing under test is ever written to.
Also: the session-lock probe took its source path from a hardcoded location, so
pointing it at a deliberately broken copy would have quietly tested the good
build and reported green. It takes the tree as a parameter now.
v9.7.0
Free TV Streams is now free, and built into every edition + Cam
connections that ride out a bad second, and a Sparks tab with no numbers
left hardcoded
NOTE: this number was used twice. Development ran on two lines, both
branched from v9.6.7: one on OHSTV and the Media Center, the other on
cam connections and Sparks. They were merged in v9.8.1, and both
accounts are kept here rather than one being dropped.
### Free TV Streams is now free, and built into every edition
FREE TV STREAMS WAS A $100 PREMIUM MOD. IT IS NOW FREE, AND BUILT IN. It ships
and turns itself on with every edition above Lite - the same as Pet World, the
Marketplace and the Cam Rooms - with nothing to buy and nothing to switch on.
The guide, the profile pin, the shared room screen and the channel share links
are all just part of the platform now.
For anyone who has not seen it: it is a browsable guide of 1,000+ free,
ad-supported US channels - news, movies, comedy, reality, classic TV, true
crime, sci-fi, kids, music, food, history, westerns and more. Members search by
category and favorite channels, pin one to their profile for visitors to press
play, and a room owner or mod can put a channel on a shared screen so a whole
cam room watches the same thing together - a member suggests one and the host
gets a one-tap PM to set it. Playback works in every browser through a built-in
server-side proxy, a health checker flags dead channels, and the admin gets a
full channel manager.
Moving it from paid to core came down to three things, and they are the part
worth writing down. The mod now ships inside every non-Lite build instead of
sitting in the paid store, so a fresh install already has it. An existing site
gets a one-time migration on update that switches it on once - and if you would
rather not run it, one toggle in Admin -> Mods turns it off and it stays off.
And the store catalog heals itself: the old $100 "Free TV Streams" listing is
stripped out on sight, so nobody is ever quoted a price for something that is
now free. The buy and upgrade pages were rewritten to match - it reads as an
included feature, not a premium mod.
### Cam connections that ride out a bad second, and a Sparks tab with no numbers left hardcoded
Everything in 9.6.6 through 9.6.9 rolled up, plus the last of the Sparks
constants. Built from one live cam_1 log; every cam change below is traceable
to a line in it.
CAM: A TEMPORARY ANSWER IS NOT A VERDICT
Three different failures in that log were the same mistake - the SFU treating a
momentary answer from Cloudflare as a permanent judgement:
15:02:47 falling back to mesh: curl: Operation timed out after 10010 ms
15:03:43 falling back to mesh: Backend error
13:20:12 track ..._video rejected: Service is temporarily unavailable.
13:20:13 pulled 2 track(s) but mapped none
The first two dropped a whole room to direct connections and each spent one of
only three SFU attempts for the visit. The third was worse: a per-track refusal
counted toward the six-try limit, and six of those mark a track dead for the
REST OF THE VISIT - so a publisher could lose their audio for an hour because a
region was busy for four seconds.
RETRIES, ON THE SERVER, WHERE THE BROWSER NEVER SEES THEM. Timeouts, resets,
empty replies, 408/425/429 and any 5xx are retried - three attempts sharing a
14-second budget, jittered so a room that all hits one blip does not come back
in lockstep and reproduce it. Retry-After is honoured. 400/401/403/404/409 are
never retried: those are real answers, and retrying them only delays the honest
error. A wrong API token still fails in one request.
THE BROWSER IS TOLD WHICH KIND OF FAILURE IT WAS and now HOLDS instead of
failing - 6s, then 9, 14, 20, 25 - spending no SFU attempt. Mesh carries the
room meanwhile and the SFU re-engages by itself. Bounded: after five holds it
takes the real fallback, so an API that is always "temporarily" broken still
stops fast.
A TEMPORARILY UNAVAILABLE TRACK IS NEVER MARKED DEAD. Those refusals no longer
touch the six-try counter. The claim goes straight back and the track parks for
a moment - 1.5s, then 3, 5, 8, 12, 15 - so an outage is not re-asked every two
seconds. Bounded too: six transient refusals and it rejoins the normal ladder,
because an unbounded cheap retry is how every step above it becomes
unreachable. A genuine "Track not found" is untouched.
Two paths that should have known this already: a dropped fetch while opening
the session, and sfu_announce's "Not in room." - the presence guard losing a
race, which the subscribe path has treated as retryable since 8.9.9. Both used
to fall back to mesh. Both now hold.
THE PHP SESSION LOCK - FOUND WHILE FIXING THE ABOVE, AND BIGGER THAN IT
session_start() runs at the top of index.php and PHP holds an exclusive lock on
the session file for the whole request. index.php never released it. So while
any handler waited on a third party, that member's own cam_heartbeat and
cam_poll were not slow - they were BLOCKED, queued on the lock. A 10s upstream
timeout also cost two missed heartbeats and a stalled roster.
Measured: holding the lock delays that member's next request by 2.10s;
releasing it first, 0.00s.
Sixteen outbound calls had this defect - TURN credentials (runs at every room
join), link unfurl, YouTube search and oEmbed, KLIPY GIF search (fired from the
chat composer), the web-push fan-out to five services at once, webhooks, two
licence checks, the security self-check, and both PayPal verifies at THIRTY
SECONDS. The release now lives inside the fetching helpers rather than at the
call sites, so a handler written later inherits it instead of re-introducing
the bug.
ONE PLACE IT DELIBERATELY DOES NOTHING: PHP cannot start a session once headers
have gone out - session_start() returns false, and stays false with the cookie
and the cache limiter both disabled. That was measured, not assumed, and the
first version of this change got it wrong: it yielded anyway, the resume failed
silently, and every $_SESSION write after the call was thrown away. The yield
declines when headers_sent() is true. Costs nothing where it matters -
cam_heartbeat, cam_poll, cam_signal and sfu_* all print nothing before their
answer, so they all take the yield.
REJOINING A ROOM NO LONGER UN-PUBLISHES YOU
13:17:55 re-advertised our SFU session - the presence row had lost it
13:18:33 re-advertised our SFU session - the presence row had lost it
Twice in 38 seconds, and three seconds after the second one that publisher's
video froze and both tracks were released.
It was cam_join. Joining removes every presence row belonging to you -
correctly, that is what kills double-cam - then appends a fresh row built from
eight fields, discarding everything any later writer added. sfu_session is one
of them. Clients re-join mid-session for ordinary reasons: a seat move, a
reconnect after a failed poll, a restored tab.
Measured with real room traffic: the publisher's row had no advertisement on 18
of 122 sampled beats before, 0 of 122 after. It rarely surfaced as that log
line because the heartbeat repair usually got there first - but in every window
between, everyone else's roster said this publisher had nothing to pull, so
they dropped their claims and released the tracks.
The carry is split on purpose. The SFU advertisement carries only when the
peer_id is unchanged; track names embed the peer id, so handing an old session
to a new peer_id would advertise tracks that cannot resolve. The timestamp
carries as its ORIGINAL value. Separately, what describes the person rather
than the peer - cam effect, room status, AFK timer, build - now survives a
reload too.
SPARKS: NOTHING LEFT HARDCODED
The graduation threshold was ALREADY settable (Admin -> Sparks -> Tuning ->
"Heat to graduate", default 12) and was verified end to end before anything was
changed, so nothing was added for it - a second control doing the same job is
the bug, not the feature.
Everything else had no control at all: nine define()s and a hardcoded byte
count, changeable only by editing the mod. All now fields in the same form,
saved by the same button:
Spark length 1200 · Reply length 600 · Replies per spark 300 · Seconds
between sparks 15 · Poll choices 6 · Choice length 70 · Feed page size 20 ·
Hall of Fame size 60 · Greatest Hits on a profile 4 · Upload size 25 MB
And the three that make the threshold mean something - what a reaction, a reply
and a poll vote are each worth (1 / 2 / 1). Floor of zero, not one: running the
feed with reactions worth nothing is a real editorial choice.
Heat is worked out from the counters each time it is read, not stored, so
changing a weight or the threshold re-scores every burning spark and can
graduate several at once. Said on the tab, not just here.
The constants are kept as the defaults, so an install that never opens the tab
behaves exactly as before. Everything is clamped twice - on save and again on
read - because a hand-edited meta.json must not be able to set a reply ceiling
of zero and make the mod unusable. Poll choices only ever narrows: with
PulsePoll installed it owns the poll and its own limit still wins.
Sparks mod 1.6.1 -> 1.8.0. Its internal SP_VER had also read 1.0.0 for six
releases; it tracks the real version now.
DOCS
docs/VARIABLES.md gains the session-lock rule (with the headers_sent caveat
that is easy to get wrong twice) and the cam_join carry table, plus a checklist
entry so the next outbound HTTP call added to this file gets the yield.
ON THE TESTS, BECAUSE THREE OF THEM WERE WRONG FIRST
A gate that passes on a broken build is worse than no gate, and this release
produced three of them before they were caught:
- The headers-sent case printed 64 bytes and believed headers were sent.
Output buffering meant they were not, so it passed with the fix removed. It
flushes for real and asserts headers_sent() in the response now.
- The Sparks page-size check counted ".sp-card", which the composer and Daily
Spark box also carry - the same number at every page size, passing on a
lever it had never exercised. It counts distinct spark card ids now.
- The cam-effect carry check posted the wrong field name, stored nothing, and
then "the effect survived" passed comparing empty with empty. It asserts the
setup took before asserting anything about it.
Every mechanism in this release is validated by sabotage - breaking it turns
specific assertions red, and those counts are recorded per suite.
left hardcoded
NOTE: this number was used twice. Development ran on two lines, both
branched from v9.6.7: one on OHSTV and the Media Center, the other on
cam connections and Sparks. They were merged in v9.8.1, and both
accounts are kept here rather than one being dropped.
### Free TV Streams is now free, and built into every edition
FREE TV STREAMS WAS A $100 PREMIUM MOD. IT IS NOW FREE, AND BUILT IN. It ships
and turns itself on with every edition above Lite - the same as Pet World, the
Marketplace and the Cam Rooms - with nothing to buy and nothing to switch on.
The guide, the profile pin, the shared room screen and the channel share links
are all just part of the platform now.
For anyone who has not seen it: it is a browsable guide of 1,000+ free,
ad-supported US channels - news, movies, comedy, reality, classic TV, true
crime, sci-fi, kids, music, food, history, westerns and more. Members search by
category and favorite channels, pin one to their profile for visitors to press
play, and a room owner or mod can put a channel on a shared screen so a whole
cam room watches the same thing together - a member suggests one and the host
gets a one-tap PM to set it. Playback works in every browser through a built-in
server-side proxy, a health checker flags dead channels, and the admin gets a
full channel manager.
Moving it from paid to core came down to three things, and they are the part
worth writing down. The mod now ships inside every non-Lite build instead of
sitting in the paid store, so a fresh install already has it. An existing site
gets a one-time migration on update that switches it on once - and if you would
rather not run it, one toggle in Admin -> Mods turns it off and it stays off.
And the store catalog heals itself: the old $100 "Free TV Streams" listing is
stripped out on sight, so nobody is ever quoted a price for something that is
now free. The buy and upgrade pages were rewritten to match - it reads as an
included feature, not a premium mod.
### Cam connections that ride out a bad second, and a Sparks tab with no numbers left hardcoded
Everything in 9.6.6 through 9.6.9 rolled up, plus the last of the Sparks
constants. Built from one live cam_1 log; every cam change below is traceable
to a line in it.
CAM: A TEMPORARY ANSWER IS NOT A VERDICT
Three different failures in that log were the same mistake - the SFU treating a
momentary answer from Cloudflare as a permanent judgement:
15:02:47 falling back to mesh: curl: Operation timed out after 10010 ms
15:03:43 falling back to mesh: Backend error
13:20:12 track ..._video rejected: Service is temporarily unavailable.
13:20:13 pulled 2 track(s) but mapped none
The first two dropped a whole room to direct connections and each spent one of
only three SFU attempts for the visit. The third was worse: a per-track refusal
counted toward the six-try limit, and six of those mark a track dead for the
REST OF THE VISIT - so a publisher could lose their audio for an hour because a
region was busy for four seconds.
RETRIES, ON THE SERVER, WHERE THE BROWSER NEVER SEES THEM. Timeouts, resets,
empty replies, 408/425/429 and any 5xx are retried - three attempts sharing a
14-second budget, jittered so a room that all hits one blip does not come back
in lockstep and reproduce it. Retry-After is honoured. 400/401/403/404/409 are
never retried: those are real answers, and retrying them only delays the honest
error. A wrong API token still fails in one request.
THE BROWSER IS TOLD WHICH KIND OF FAILURE IT WAS and now HOLDS instead of
failing - 6s, then 9, 14, 20, 25 - spending no SFU attempt. Mesh carries the
room meanwhile and the SFU re-engages by itself. Bounded: after five holds it
takes the real fallback, so an API that is always "temporarily" broken still
stops fast.
A TEMPORARILY UNAVAILABLE TRACK IS NEVER MARKED DEAD. Those refusals no longer
touch the six-try counter. The claim goes straight back and the track parks for
a moment - 1.5s, then 3, 5, 8, 12, 15 - so an outage is not re-asked every two
seconds. Bounded too: six transient refusals and it rejoins the normal ladder,
because an unbounded cheap retry is how every step above it becomes
unreachable. A genuine "Track not found" is untouched.
Two paths that should have known this already: a dropped fetch while opening
the session, and sfu_announce's "Not in room." - the presence guard losing a
race, which the subscribe path has treated as retryable since 8.9.9. Both used
to fall back to mesh. Both now hold.
THE PHP SESSION LOCK - FOUND WHILE FIXING THE ABOVE, AND BIGGER THAN IT
session_start() runs at the top of index.php and PHP holds an exclusive lock on
the session file for the whole request. index.php never released it. So while
any handler waited on a third party, that member's own cam_heartbeat and
cam_poll were not slow - they were BLOCKED, queued on the lock. A 10s upstream
timeout also cost two missed heartbeats and a stalled roster.
Measured: holding the lock delays that member's next request by 2.10s;
releasing it first, 0.00s.
Sixteen outbound calls had this defect - TURN credentials (runs at every room
join), link unfurl, YouTube search and oEmbed, KLIPY GIF search (fired from the
chat composer), the web-push fan-out to five services at once, webhooks, two
licence checks, the security self-check, and both PayPal verifies at THIRTY
SECONDS. The release now lives inside the fetching helpers rather than at the
call sites, so a handler written later inherits it instead of re-introducing
the bug.
ONE PLACE IT DELIBERATELY DOES NOTHING: PHP cannot start a session once headers
have gone out - session_start() returns false, and stays false with the cookie
and the cache limiter both disabled. That was measured, not assumed, and the
first version of this change got it wrong: it yielded anyway, the resume failed
silently, and every $_SESSION write after the call was thrown away. The yield
declines when headers_sent() is true. Costs nothing where it matters -
cam_heartbeat, cam_poll, cam_signal and sfu_* all print nothing before their
answer, so they all take the yield.
REJOINING A ROOM NO LONGER UN-PUBLISHES YOU
13:17:55 re-advertised our SFU session - the presence row had lost it
13:18:33 re-advertised our SFU session - the presence row had lost it
Twice in 38 seconds, and three seconds after the second one that publisher's
video froze and both tracks were released.
It was cam_join. Joining removes every presence row belonging to you -
correctly, that is what kills double-cam - then appends a fresh row built from
eight fields, discarding everything any later writer added. sfu_session is one
of them. Clients re-join mid-session for ordinary reasons: a seat move, a
reconnect after a failed poll, a restored tab.
Measured with real room traffic: the publisher's row had no advertisement on 18
of 122 sampled beats before, 0 of 122 after. It rarely surfaced as that log
line because the heartbeat repair usually got there first - but in every window
between, everyone else's roster said this publisher had nothing to pull, so
they dropped their claims and released the tracks.
The carry is split on purpose. The SFU advertisement carries only when the
peer_id is unchanged; track names embed the peer id, so handing an old session
to a new peer_id would advertise tracks that cannot resolve. The timestamp
carries as its ORIGINAL value. Separately, what describes the person rather
than the peer - cam effect, room status, AFK timer, build - now survives a
reload too.
SPARKS: NOTHING LEFT HARDCODED
The graduation threshold was ALREADY settable (Admin -> Sparks -> Tuning ->
"Heat to graduate", default 12) and was verified end to end before anything was
changed, so nothing was added for it - a second control doing the same job is
the bug, not the feature.
Everything else had no control at all: nine define()s and a hardcoded byte
count, changeable only by editing the mod. All now fields in the same form,
saved by the same button:
Spark length 1200 · Reply length 600 · Replies per spark 300 · Seconds
between sparks 15 · Poll choices 6 · Choice length 70 · Feed page size 20 ·
Hall of Fame size 60 · Greatest Hits on a profile 4 · Upload size 25 MB
And the three that make the threshold mean something - what a reaction, a reply
and a poll vote are each worth (1 / 2 / 1). Floor of zero, not one: running the
feed with reactions worth nothing is a real editorial choice.
Heat is worked out from the counters each time it is read, not stored, so
changing a weight or the threshold re-scores every burning spark and can
graduate several at once. Said on the tab, not just here.
The constants are kept as the defaults, so an install that never opens the tab
behaves exactly as before. Everything is clamped twice - on save and again on
read - because a hand-edited meta.json must not be able to set a reply ceiling
of zero and make the mod unusable. Poll choices only ever narrows: with
PulsePoll installed it owns the poll and its own limit still wins.
Sparks mod 1.6.1 -> 1.8.0. Its internal SP_VER had also read 1.0.0 for six
releases; it tracks the real version now.
DOCS
docs/VARIABLES.md gains the session-lock rule (with the headers_sent caveat
that is easy to get wrong twice) and the cam_join carry table, plus a checklist
entry so the next outbound HTTP call added to this file gets the yield.
ON THE TESTS, BECAUSE THREE OF THEM WERE WRONG FIRST
A gate that passes on a broken build is worse than no gate, and this release
produced three of them before they were caught:
- The headers-sent case printed 64 bytes and believed headers were sent.
Output buffering meant they were not, so it passed with the fix removed. It
flushes for real and asserts headers_sent() in the response now.
- The Sparks page-size check counted ".sp-card", which the composer and Daily
Spark box also carry - the same number at every page size, passing on a
lever it had never exercised. It counts distinct spark card ids now.
- The cam-effect carry check posted the wrong field name, stored nothing, and
then "the effect survived" passed comparing empty with empty. It asserts the
setup took before asserting anything about it.
Every mechanism in this release is validated by sabotage - breaking it turns
specific assertions red, and those counts are recorded per suite.
v9.6.9
Sparks: every limit is now an admin setting
FIRST, THE ONE THAT PROMPTED THIS: the graduation threshold was ALREADY
settable. Admin -> ⚡ Sparks -> Tuning -> "Heat to graduate", default 12. It
renders, it saves, and it is read by the only thing that decides graduation
(sp_grad_heat). That was checked end to end on a live sandbox before changing
anything, and nothing was added for it - a second control doing the same job
would have been the bug, not the feature.
WHAT GENUINELY HAD NO CONTROL was everything else. Nine define()s and a
hardcoded byte count, changeable only by editing the mod file:
Spark length 1200 characters
Reply length 600 characters
Replies per spark 300 hard ceiling
Seconds between sparks 15 per member
Poll choices 6 per spark
Poll choice length 70 characters
Sparks per feed page 20
Hall of Fame size 60
Greatest Hits on profile 4
Upload size 25 MB
All ten are now fields in the same Tuning form, saved by the same button. The
constants are kept as the defaults, so an install that never touches the tab
behaves exactly as it did.
CLAMPED IN TWO PLACES ON PURPOSE. The form clamps on save, and each accessor
clamps again when it reads. The second one is not redundant: a hand-edited
meta.json must not be able to set a reply ceiling of zero and make the mod
unusable.
POLL CHOICES ONLY EVER NARROWS. When PulsePoll is installed it owns the poll
and its own admin limit still wins; the Sparks setting can only take it lower.
Said on the tab, not just here.
THE TEST CHECKS THE LEVER, NOT THE FIELD. A setting that renders and saves but
changes nothing is the failure worth testing for, so every limit is checked
twice: the value round-trips to disk and back into the form, AND the behaviour
moves when it changes. A spark over the new length is refused and the same
spark posts once the limit is raised; the cooldown blocks a second spark and
zero lets it through; a page size of five shows five sparks.
That last one caught a bad test rather than bad code: the first version counted
".sp-card", which the composer and the Daily Spark box also carry, so it
returned the same number at every page size and the check passed on a lever it
had never actually exercised. It counts distinct spark card ids now. Sabotaging
three of the mechanisms turns the gate 8 red.
Sparks mod 1.6.1 -> 1.7.0.
v9.6.8
Free TV Streams: a share link for every channel + Rejoining a room
no longer un-publishes you
NOTE: this number was used twice. Development ran on two lines, both
branched from v9.6.7: one on OHSTV and the Media Center, the other on
cam connections and Sparks. They were merged in v9.8.1, and both
accounts are kept here rather than one being dropped.
### Free TV Streams: a share link for every channel
EVERY CHANNEL NOW HAS A SHARE LINK OF ITS OWN. The link button turns up in four
places - on each card in the guide, on the player bar, on the in-room TV window
while a channel is playing, and on the profile card of anyone who has pinned
one. One tap copies the link, or opens the phone's native share sheet where
there is one, with a plain copy box as the fallback everywhere else.
Open one of those links and you land on the guide with that exact channel
already playing, guest or member alike. The part that makes it work is where the
link points: it is built from your own site's address - the canonical URL you
set in admin if there is one, otherwise the domain the visitor is actually on -
so a link copied on your site always points back at your site. Paste it into
chat, a bulletin, a post, or anywhere off-site, and it brings people home to the
right channel.
### Rejoining a room no longer un-publishes you
The last unexplained line from the cam_1 log:
13:17:55 re-advertised our SFU session - the presence row had lost it
13:18:33 re-advertised our SFU session - the presence row had lost it
Twice in 38 seconds for the same person, and three seconds after the second
one, Maddog's video froze for them and both tracks were released. That
sequence is the whole failure: lost advertisement, then freeze, then a re-pull
that ran into the upstream 503 already handled in 9.6.6.
IT WAS cam_join. Joining a room removes every presence row belonging to you -
correctly, that is what kills double-cam - and then appends a fresh row built
from eight fields. Every field any later writer added is thrown away with the
old row, and sfu_session is one of them. A client re-joins mid-session for
entirely ordinary reasons: a seat move, a reconnect after a failed poll, a
restored tab.
v9.1.5 fixed the same failure for a PRUNE three lines above this code and its
comment spells out why it matters - a publisher announces exactly once, so a
row rebuilt without the advertisement leaves them unpullable and they never
find out. The deliberate replacement at join was never given the same
treatment.
MEASURED, NOT ARGUED. Driving the real handlers with the traffic a busy room
makes - three peers heartbeating and polling, one publishing, rejoining
periodically - the publisher's row was found with no advertisement on 18 of
122 sampled beats before the fix and 0 of 122 after. It rarely showed up as
the log line because 9.4.1's heartbeat repair usually got there first and put
it back; but for every window in between, every other client's roster said
this publisher had nothing to pull, so they dropped their claims and released
the tracks. That is the "released N track(s) ... will re-pull" churn.
WHAT CARRIES, AND THE SPLIT IS DELIBERATE. The SFU advertisement carries only
when the peer_id is unchanged - a rejoin from the same live page. Track names
embed the peer id, so handing a previous peer's session to a NEW peer_id would
advertise tracks that cannot resolve, which is worse than advertising nothing;
a page reload mints a fresh peer_id and announces again on its own. The
timestamp carries as the ORIGINAL value, not reset to now, so anything
reasoning about the age of an advertisement is not told it is brand new.
Separately, the fields that describe the PERSON rather than the peer - cam
effect, room status, AFK timer, build - now survive a reload too. They were
being reset by the same rebuild.
A NOTE ON THE TEST, because the same trap keeps coming round: the first
version of the effect check posted the wrong parameter name, stored nothing,
and then "the effect survived the reload" passed by comparing empty with
empty. It now asserts the setup actually took before asserting anything about
it. Removing the carry turns the gate 4 red, including 33 lost samples under
load.
NOTE: this number was used twice. Development ran on two lines, both
branched from v9.6.7: one on OHSTV and the Media Center, the other on
cam connections and Sparks. They were merged in v9.8.1, and both
accounts are kept here rather than one being dropped.
### Free TV Streams: a share link for every channel
EVERY CHANNEL NOW HAS A SHARE LINK OF ITS OWN. The link button turns up in four
places - on each card in the guide, on the player bar, on the in-room TV window
while a channel is playing, and on the profile card of anyone who has pinned
one. One tap copies the link, or opens the phone's native share sheet where
there is one, with a plain copy box as the fallback everywhere else.
Open one of those links and you land on the guide with that exact channel
already playing, guest or member alike. The part that makes it work is where the
link points: it is built from your own site's address - the canonical URL you
set in admin if there is one, otherwise the domain the visitor is actually on -
so a link copied on your site always points back at your site. Paste it into
chat, a bulletin, a post, or anywhere off-site, and it brings people home to the
right channel.
### Rejoining a room no longer un-publishes you
The last unexplained line from the cam_1 log:
13:17:55 re-advertised our SFU session - the presence row had lost it
13:18:33 re-advertised our SFU session - the presence row had lost it
Twice in 38 seconds for the same person, and three seconds after the second
one, Maddog's video froze for them and both tracks were released. That
sequence is the whole failure: lost advertisement, then freeze, then a re-pull
that ran into the upstream 503 already handled in 9.6.6.
IT WAS cam_join. Joining a room removes every presence row belonging to you -
correctly, that is what kills double-cam - and then appends a fresh row built
from eight fields. Every field any later writer added is thrown away with the
old row, and sfu_session is one of them. A client re-joins mid-session for
entirely ordinary reasons: a seat move, a reconnect after a failed poll, a
restored tab.
v9.1.5 fixed the same failure for a PRUNE three lines above this code and its
comment spells out why it matters - a publisher announces exactly once, so a
row rebuilt without the advertisement leaves them unpullable and they never
find out. The deliberate replacement at join was never given the same
treatment.
MEASURED, NOT ARGUED. Driving the real handlers with the traffic a busy room
makes - three peers heartbeating and polling, one publishing, rejoining
periodically - the publisher's row was found with no advertisement on 18 of
122 sampled beats before the fix and 0 of 122 after. It rarely showed up as
the log line because 9.4.1's heartbeat repair usually got there first and put
it back; but for every window in between, every other client's roster said
this publisher had nothing to pull, so they dropped their claims and released
the tracks. That is the "released N track(s) ... will re-pull" churn.
WHAT CARRIES, AND THE SPLIT IS DELIBERATE. The SFU advertisement carries only
when the peer_id is unchanged - a rejoin from the same live page. Track names
embed the peer id, so handing a previous peer's session to a NEW peer_id would
advertise tracks that cannot resolve, which is worse than advertising nothing;
a page reload mints a fresh peer_id and announces again on its own. The
timestamp carries as the ORIGINAL value, not reset to now, so anything
reasoning about the age of an advertisement is not told it is brand new.
Separately, the fields that describe the PERSON rather than the peer - cam
effect, room status, AFK timer, build - now survive a reload too. They were
being reset by the same rebuild.
A NOTE ON THE TEST, because the same trap keeps coming round: the first
version of the effect check posted the wrong parameter name, stored nothing,
and then "the effect survived the reload" passed by comparing empty with
empty. It now asserts the setup actually took before asserting anything about
it. Removing the carry turns the gate 4 red, including 33 lost samples under
load.
v9.6.7
The session lock, everywhere else it was doing that
9.6.6 found that PHP holds an exclusive lock on the session file for a whole
request and that index.php never released it - so while an SFU call waited on
Cloudflare, that same member's cam_heartbeat and cam_poll were not slow, they
were BLOCKED behind it. That release was applied to the four sfu_* handlers
and nowhere else. This finishes the job.
SIXTEEN OUTBOUND CALLS HAD THE SAME DEFECT. A TURN credential fetch (6s, runs
on the ICE endpoint at every room join), a link unfurl (6s, and it follows
redirects), a YouTube search or oEmbed title lookup (8s and 3s), a KLIPY GIF
search (8s, fired straight from the chat composer), a web-push fan-out to five
services at once, a webhook, two licence checks, the security self-check, and
two PayPal verifies with a THIRTY SECOND timeout. Every one of them could stall
that member's cam heartbeat and room poll for its full duration.
THE FIX GOES IN THE HELPERS, NOT AT THE CALL SITES. ohs_session_yield() drops
the lock immediately before the network wait and ohs_session_resume() picks it
back up immediately after. Putting it inside the functions that do the fetching
is the whole point: a handler written next month inherits it instead of
re-introducing the bug. Nothing touches $_SESSION during a curl_exec, so
nothing can be lost in the window, and anything written before the yield was
flushed to disk by the close.
ONE PLACE IT DELIBERATELY DOES NOTHING, AND THIS IS THE INTERESTING PART. PHP
cannot start a session once headers have gone out - session_start() simply
returns false, and it keeps returning false with the session cookie and the
cache limiter both disabled. That was measured here, not assumed, and the first
version of this change got it wrong: it yielded anyway, the resume failed
silently, and every $_SESSION write after the call was thrown away. So the
yield now declines when headers_sent() is true. A call made midway through
printing a page keeps the lock, exactly as it did before this existed.
That costs nothing where it matters. Everything that contends for the lock in a
cam room - cam_heartbeat, cam_poll, cam_signal, sfu_* - is a JSON handler that
prints nothing before its answer, so all of them take the yield.
WHY THE FIRST TEST OF THIS WAS WORTHLESS, since the same trap will come round
again: output buffering means an echo does NOT send headers. The first version
printed 64 bytes, believed it was testing the headers-sent path, and passed
with the fix deliberately removed. It now flushes for real and asserts
headers_sent() in the response before making any claim about that case. The
concurrency half needs PHP_CLI_SERVER_WORKERS set, or php -S serialises
everything and the test fails for a reason that has nothing to do with the code.
One mechanism, not two: 9.6.6's ohs_sfu_release_session() now delegates to
ohs_session_yield() rather than carrying its own copy. It still never resumes,
because nothing after that point in an sfu_* handler touches the session.
v9.6.6
Cam connection hardening: a bad second upstream is no longer a verdict
Built off a live cam_1 log. Every change below is traceable to a line in it.
THE PATTERN. Three different failures in that log were all the same mistake:
the SFU treated a TEMPORARY answer from Cloudflare as a permanent judgement
about whether the SFU works.
15:02:47 falling back to mesh: curl: Operation timed out after 10010 ms
15:03:43 falling back to mesh: Backend error
13:20:12 track ..._video rejected: Service is temporarily unavailable.
13:20:13 pulled 2 track(s) but mapped none
The first two dropped a whole room to direct connections and each spent one of
only three SFU attempts for the visit. The third was worse than it looks: a
per-track refusal counted toward the six-try limit, and six of them mark that
track dead for the REST OF THE VISIT. A publisher could lose their audio for
an hour because a Cloudflare region was busy for four seconds.
RETRIES, ON THE SERVER, WHERE THE BROWSER NEVER SEES THEM. Requests to the
Realtime API are now classified before anyone acts on them. A timeout, a reset
connection, an empty reply, a 408/425/429, or any 5xx is retried - up to three
attempts sharing a 14-second budget, with a jittered back-off so a room full of
clients that all hit the same blip do not come back in lockstep and reproduce
it. A 429's Retry-After header is honoured. A 400, 401, 403, 404 or 409 is NOT
retried: those are real answers about a real problem, and retrying them only
delays the honest error. A wrong API token still fails in one request.
AND THE PHP SESSION LOCK IS RELEASED FIRST. This one is the reason the retries
are safe, and it was a live problem before them. PHP holds an exclusive lock on
the session file for a whole request and index.php never released it, so while
an SFU call sat waiting on Cloudflare, that same member's cam_heartbeat and
cam_poll were not slow - they were BLOCKED behind it. A 10-second upstream
timeout therefore also cost two missed heartbeats and a stalled roster, which
is the presence-row churn that follows those lines in the log. Measured here:
holding the lock delays the next request by 2.10s, releasing it first by 0.00s.
The four SFU handlers now release the session before they call the API.
THE BROWSER IS TOLD WHICH KIND OF FAILURE IT WAS. Transient answers come back
flagged, and the client now HOLDS instead of failing: it waits 6s, then 9, 14,
20, 25 - and spends no SFU attempt doing it. Mesh carries the room meanwhile,
exactly as it does today, and the SFU re-engages by itself. This is bounded:
after five holds the SFU takes the real fallback, so an API that is always
"temporarily" broken still stops fast and says why it stopped believing that.
A TEMPORARILY UNAVAILABLE TRACK IS NEVER MARKED DEAD. Those rejections no
longer touch the six-try counter at all. The claim is given straight back and
the track is parked for a moment - 1.5s, then 3, 5, 8, 12, 15 - so an outage is
not re-asked every two seconds, and one line is logged rather than one per
track per beat. That is bounded too: six transient refusals and it rejoins the
normal ladder, because an unbounded cheap retry is how every step above it
becomes unreachable. A genuine "Track not found on remote peer" is untouched
and still reaches the mesh hand-off after six tries.
TWO PLACES THAT SHOULD HAVE KNOWN THIS ALREADY. A dropped fetch while opening
the session used to fall back to mesh; sync() has treated that as a transport
failure rather than a protocol one since 8.9.8, and start() never learned it.
And sfu_announce makes no upstream call at all - its "Not in room." is the
presence guard losing a race with a prune, which is the same race the subscribe
path has treated as retryable since 8.9.9. Both now hold instead of failing.
CLEARER LOG LINES. An upstream failure and a roster race used to print the same
"subscribe raced the roster" text. They are different events with different
causes and they now read differently, so the panel points at the right thing.
Settings, if they are ever needed: sfu_retry_attempts (default 3) and
sfu_retry_budget_ms (default 14000). Neither needs touching.
WHAT WAS LOOKED AT AND DELIBERATELY NOT CHANGED. The log's republish at
15:41:20 followed the governor easing for 23.8% packet loss five seconds
earlier. An ICE restart would be the cheap step before a full rebuild there,
but restartIce() only takes effect on the next offer WE send, and this module
never sends another one - the Realtime API's renegotiate endpoint takes our
answer, not our offer. Adding it would have been a control wired to nothing,
so the rebuild stays as it is. The 23.8% loss itself is a real network event,
not something the code caused.
v9.6.5
The KLIPY GIF picker
9.6.4 swapped the trusted-host list from Tenor to KLIPY so pasted links kept
working. This adds the search: type words in the chat's GIF box, pick a
result, it posts to the room.
NO NEW BUTTON AND NO SECOND ENTRY POINT. The GIF popover that already exists
in the chat composer now does both jobs. What decides which one you get is
whether the text looks like a URL: words search KLIPY, anything starting
http(s):// takes the paste path, byte-for-byte as before. Opening the popover
shows trending, the same way the room player's search does. This mirrors the
YouTube share popover sitting directly beneath it rather than inventing a
second pattern.
THE API KEY IS A SETTING, NOT A CONSTANT. Admin -> Rooms -> GIF search (KLIPY).
Two reasons it is not baked into code: the buyer-package builder regenerates
every buyer zip from the deployed index.php, so a hardcoded key would ship to
every buyer; and the browser never needs it, because the picker calls this
site's own ?action=klipy_search and the server calls KLIPY. Same rule the
Cloudflare TURN token has followed since 8.4.2. The field is masked exactly
like smtp_pass - a blank submit keeps the stored key, and the value is never
echoed back into the page. Leave the key empty and the popover behaves exactly
as it did in 9.6.4.
THE PARSER IS SHAPE-TOLERANT ON PURPOSE. KLIPY documents the envelope
({result, data:{data:[...]}}) but the per-item media object is documented only
on a JavaScript-rendered page, and format bucket names have changed before.
Instead of hard-coding files.hd.gif.url and silently returning nothing the day
a bucket is renamed, the parser walks whatever object it is handed and takes
the best image URL on it - preferring a still GIF over WebP over MP4, and a
mid-size render over a huge one, because these land in a chat bubble.
PROVENANCE, INSTEAD OF WIDENING THE ALLOW-LIST. send_chat only accepts a GIF
URL whose host is on its trusted list, and KLIPY's media hostnames are not
published anywhere machine-readable. Rather than guess a hostname into a
security check, every URL this server receives from the KLIPY API is signed
with the site secret, and send_chat accepts an off-list host only when that
signature verifies. A member cannot mint one - the secret never leaves the
server - so the signature proves the URL came out of our own API call rather
than being typed into the box. Signed URLs also skip the .gif/.webp extension
test, because KLIPY renders do not always carry an extension. Everything
pasted by hand still goes the long way round, unchanged.
A TEST BUTTON, BECAUSE THERE IS NO SHELL. Admin -> Rooms has a "Test the
connection" button beside the key. It clears the cache, runs one live search
and reports what came back, including the media hostname KLIPY actually served
- so a bad key or an unexpected host is visible from the admin screen instead
of needing a server login.
Searches are rate-limited to 20 a minute per member, results cache for a day,
the cache is cleared whenever the key changes, and the whole feature is gated
behind OHS_CAMS_OK so nothing about it appears on Lite. KLIPY's attribution
sits under the search field and results as their terms require.
VERIFIED. 59/59 PHP files lint clean. 52 pages and all 42 admin tabs return
200 with zero PHP diagnostics. 19 assertions run against the real shipped
functions, lifted out of index.php with token_get_all and fed a KLIPY-shaped
payload: the parser prefers the mid-size GIF over HD and MP4, finds WebP under
a differently-named bucket, finds media under a key name it has never seen,
falls back to MP4 when there is no still image, and drops an item with nothing
usable. A signature the server minted verifies; a forged one, an empty one and
one moved to a different URL are all refused. Signed off-list media and signed
media with no file extension are accepted; the same URLs unsigned are refused;
Giphy pastes still work and an untrusted host and a klipy.com.evil.net
lookalike are still refused. The suite was validated by sabotage - stubbing the
signature check to return true turns three assertions red.
The key was saved through the real admin form and then swept for: it appears
on no page a member or admin can load, and the field renders masked. The
picker markup only exists when a key is stored.
NOT VERIFIED, AND SAID PLAINLY: this container cannot reach api.klipy.com, so
no request has been made to the live API. Everything above is tested against a
synthetic response built to KLIPY's documented shape. Press "Test the
connection" after deploying - that is what confirms the real thing.
v9.6.4
An emoji font, and KLIPY replaces Tenor
THE ICONS AREN'T MISSING, THE FONT IS. 9.6.3 swapped out every Emoji 14/15
glyph after the Support lifebuoy turned up as an empty box. Then the Store
entry in the top menu turned out to be blank too - and that one is a coin,
U+1FA99, Emoji 13.0 from 2020. Same class of failure, one Unicode generation
lower.
A full sweep found 234 occurrences across 40 glyphs that a 2019-era emoji
font cannot draw: the token coin in 99 places, chairs, buckets, mirrors,
potted plants, the ice cube, and every one of the coloured status circles.
SUBSTITUTION WAS THE WRONG TOOL. Replacing all of those means 40 judgement
calls, and several have no honest pre-2018 equivalent - there is no green
circle before Emoji 12, so the Reversi piece and the "uncommon" rarity dot
would both have had to become something they are not. And it only ever fixes
the glyphs that happen to be in this build; a mod that ships a newer emoji
breaks again.
WHAT SHIPPED INSTEAD. Noto Color Emoji now loads as the last entry in the
site's font stacks. Google Fonts serves it in unicode-range slices, so a
browser downloads only the slices for glyphs it actually has to draw, and a
machine with a current emoji font downloads nothing at all. It sits last in
every stack, so a real system emoji always wins. The font is added to the body
stack, the heading stack, and to form controls and nav items, because a
font-family declaration replaces the inherited stack rather than extending it.
The 41 glyph swaps from 9.6.3 are left as they are.
KLIPY REPLACES TENOR FOR GIF SHARING. Google shut the Tenor API down on
30 June 2026 and pasted tenor.com media stopped resolving with it. This build
never called the Tenor API - the only Tenor code in it was the trusted-host
allow-list that GIF URLs are checked against - so the change is to that list,
in all three places it exists: the server-side regex set and host list in
send_chat, the GIF popover in the cam room, and the paste-a-link handler in
the chat box.
KLIPY is matched on its apex with a suffix test, so every media subdomain they
serve from is covered without this list having to know each one. Giphy, Imgur,
Cloudinary and Unsplash are unchanged.
ONE INCONSISTENCY FIXED WHILE IN THERE. The paste-a-link handler tested the
host with an exact match while the GIF popover used a suffix match. An apex-
only test would have accepted a link from klipy.com but dropped one from a
klipy.com media subdomain into the chat box as plain text. Both now use the
same test.
VERIFIED. 59/59 PHP files lint clean. 52 pages and all 42 admin tabs return
200 with zero PHP diagnostics. The GIF validator was run against ten URLs:
KLIPY apex, media subdomain and CDN subdomain with a query string are all
accepted; a retired Tenor URL, an untrusted host, a klipy.com.evil.net
lookalike, a page link with no file extension, and a plain-http URL are all
refused; Giphy and Imgur still pass. The Lite edition was re-derived from the
patched master and differs by exactly 14 lines, with no mailbox credentials
and the buyer-package builder's edition marker intact.
NOTE ON PASTING. The validator has always required a direct media URL ending
in .gif or .webp - a page link will be refused. Use "copy image address", not
the address bar. The help text under the GIF box now says so.
v9.6.3
Icons that drew as empty boxes
THE SUPPORT ICON WAS NOT MISSING - IT WAS UNPRINTABLE. The lifebuoy beside
the footer's "Support" link is U+1F6DF, added to Unicode in Emoji 14.0 in
2021. Any system whose emoji font predates that - Windows 10 before 21H2,
Android 11 and earlier, older iOS - has no glyph for it, so it draws as an
empty box or as nothing at all while the word beside it renders normally.
Nothing was broken server-side: the character was being emitted correctly
and simply could not be drawn.
WHAT CHANGED. Every Emoji 14.0 and 15.0 glyph in the build was replaced with
a pre-2017 equivalent, which is on every emoji font shipped in the last
decade:
lifebuoy -> SOS (Support links)
id card -> card index (profile facts, Sparks settings)
bubbles -> droplet (Cam FX chip, store items)
jar -> amphora (Kush Kingdom curing jars)
disco ball -> dancer (store item)
melting face -> dizzy face (mood picker)
face holding back tears -> relieved (mood picker)
playground slide -> roller coaster (Little Life)
wheel -> carousel horse (Pet World)
lotus -> cherry blossom (Little Life)
Forty-one in total, across index.php, the admin guide, Kush Kingdom, Sparks,
four Add-on Store seed items and the buyer_assets mod copies. Four of those
seed icons were stored as \uD83E\uDEE7 surrogate escapes rather than literal
characters, so a plain character search does not find them.
THE FLOOR IS EMOJI 14.0, ON PURPOSE. Emoji 12 and 13 glyphs are present on
the same fonts that lack 14 - that includes the token coin (110 uses across
the site), the capsule headstone and the Ops tab stethoscope. Remapping those
would change familiar icons for no gain, so they were left exactly as they
were.
MODS ARE COVERED TOO. Core echoes HTML built by mods in three places: the
Help Desk footer link, the client portal's Support tab, and the dispatch that
renders any mod-owned page (which is how ?page=support draws). A new top-level
helper, ohs_glyph_safe(), maps the same set on the way out at those three
points, so a mod that ships a 2021 emoji still renders on a 2019 font without
that mod needing an update. It is declared unconditionally at column 0 - a
conditional declaration is not hoisted, and both call sites sit far above
where a nested definition would land.
VERIFIED. 59/59 PHP files lint clean. 52 pages and all 42 admin tabs return
200 with zero PHP diagnostics. Nineteen pages scanned in their rendered output
carry zero Emoji 14/15 glyphs. The footer link and the support page were
measured before and after against a Help Desk stand-in: 1 and 2 undrawable
glyphs before, zero after, with the safe glyph in their place. The Lite
edition was re-derived from the patched master and differs by exactly 14
lines, with no mailbox credentials and the buyer-package builder's edition
marker intact.
v9.6.1
The overlays that "didn't work", and a Matrix made of actual Matrix
FOURTEEN OF THE TWENTY-ONE ANIMATED OVERLAYS WERE JUMPING ONCE A CYCLE, and
one of them was not the effect it claimed to be. Both are fixed. The add-on
was correct and has not been touched - the site engine now matches it.
WHY THEY JUMPED. The downloadable paints overlays on a canvas and wraps the
offset on every frame:
ox = ((k * dx) % tileW + tileW) % tileW
so its layers drift forever and physically cannot snap. The site paints the
same overlays as CSS on the cam tile, and a CSS background-position animation
has no wrap - it runs from zero to the target across the cycle and then RESETS
TO ZERO. That only looks continuous when the distance travelled is an exact
whole multiple of the tile, on BOTH axes. Anything else jumps by the remainder,
once per cycle, forever. Rain jumped 24px sideways nearly twice a second.
Snow, bubbles, confetti, hearts, petals, leaves, storm and fire all did the
same, more slowly.
EVERY TRAVEL DISTANCE IS NOW A WHOLE MULTIPLE. The small sideways drifts are
gone because every one of them was under a third of a tile - keeping them
meant a visible sideways snap several times a second, and the diagonal
character of rain, leaves and petals comes from the gradient angle and the
shape rotation, not from the drift.
THREE WERE WORSE THAN MISCUT:
* EMBERS had six layers but only four sets of offsets. CSS silently repeats
a short list, so layers five and six were being driven by layers one and
two's numbers - and not one of the six travelled a whole tile anyway. The
entire field jumped every four seconds. Six layers, six offsets, all exact.
* UNDERWATER'S BUBBLES borrowed the main bubble field's offsets, which are
cut for different tiles. It was travelling 2.86 and 3.47 tiles and
snapping back every 5.5 seconds. It has its own keyframe now.
* BOKEH had five layers and four offsets, so its fifth layer drifted exactly
like its first and the parallax it exists for was cancelled out. Fifth
offset added.
SPARKLE no longer drifts at all, which is what the add-on does - it twinkles
in place. The site was sliding it a fraction of a tile and snapping back every
14 seconds.
FOG was positioned in percentages against percentage-sized layers, which
travels a fraction of the image and resets. It scrolls pixel tiles by exactly
their own width now, in opposite directions, like the add-on's two fog layers.
MATRIX WAS NOT THE SAME EFFECT AT ALL. The site drew two gradients - fine
vertical green stripes and a sliding band - and no characters whatsoever, so
it read as green venetian blinds. The add-on paints the glyph set 01<>{}[]/\|=+*
into two tiles at 14px and 16px monospace, stepping 18px and 20px down the
tile, fading with depth, falling one whole tile per cycle at 2.5s and 1.6s.
Those exact glyphs, positions, sizes, opacities and speeds are now reproduced
on the site as two SVG tiles. Two layers moving at different speeds need two
elements, because one element has only one background-position to animate,
so the second layer rides the ::before.
UNTOUCHED ON PURPOSE: fireflies, plasma, bokeh's drift, static and underwater's
caustics all use `alternate`, which reverses instead of resetting, so they
never jumped. Static is stepped on purpose - that is what TV noise is. Stars
was already cut correctly.
v9.6.0
The site tells you when something happened, and members can report
TWO THINGS, BOTH OF WHICH THE SITE HAS BEEN QUIETLY MISSING.
1. THE SOCIAL ACTIONS NOW REACH THE PERSON THEY HAPPENED TO
Six things could happen to you on this site without you ever finding out.
Someone followed you, commented on your profile, signed your guestbook, liked
your bulletin, posted a story, or replied to your event - and nothing was sent.
No bell, no badge, no push. The only way to learn about any of it was to go
and look.
The delivery machinery was never the problem. createNotification has always
written the bell row, capped it at 200 per member and handed off to browser
push. What was missing was anything calling it. Those six now do:
* Someone follows you -> "@name started following you"
* Someone comments on your page -> the comment, quoted
* Someone signs your guestbook -> the entry, quoted
* Someone likes your post -> which post, by name
* Someone you follow posts a story
* Someone RSVPs to your event -> going / maybe / can't make it
Plus one that is new: WHEN SOMEONE YOU FOLLOW GOES LIVE IN A ROOM, you are
told. The homepage has had a "Friends Live Now" card for a while, but it only
worked if you were already sitting on the homepage looking at it. Now the fact
comes to you, and tapping it puts you in the room.
Deliberate limits on that one, because an alert nobody wants is worse than no
alert at all:
- One live alert per member per 30 minutes, across every room. Somebody
hopping four rooms does not ping their followers four times.
- Private rooms never announce. Unlisted rooms never announce.
- Lurkers never announce - if you slipped in without camming, nobody is told.
TWO NEW SWITCHES in Settings -> Notifications, both on by default:
"Comments, guestbook signings, likes, new followers and event replies"
"Stories from people I follow, and when they go live in a room"
Keep the direct ones and mute the feed-ish ones, or the other way round.
THREE SETTINGS CHECKBOXES THAT DID NOTHING NOW DO SOMETHING. "Email me about
friend requests" and "email me about comments" have been on the Settings page
since v5.5.0 saving a value that no code ever read - the only email the site
ever actually sent was for private messages. Both now have senders behind
them.
Under the hood: a notification going to many people at once (a story, a live
alert) is written in ONE locked pass over notifications.json instead of one
full read-and-rewrite per recipient. A member with 300 followers used to mean
300 complete rewrites of a growing file inside a single page load. It is now
one. Measured at 300 recipients: well under a second, and every row carries
the same timestamp because there was only ever one write. The fan-out types
are also kept out of browser push on purpose - push is one HTTP request per
subscribed device, and 300 of those inline would stall the request that
triggered them. Direct, person-to-person alerts still push exactly as before.
2. MEMBERS CAN REPORT THINGS
The Terms have told members to "report it to us using the contact information
below" ever since the room-responsibility rewrite. The mechanism behind that
sentence was an email address. Now it is a button.
A folded-away Report control now sits on every member-authored surface:
profiles, profile comments, guestbook entries, bulletins (both the board and
the permalink), the room directory, and - inside a cam room - the user menu
you already right-click to ignore or ban someone, with the room attached so
the moderator knows where it happened. Stories get one too, in the viewer,
because a story is gone in 24 hours and reporting one has to be possible while
it is still on screen.
Ten reasons to pick from, including "involves someone under 18" and "self-harm,
or someone who may be at risk". An optional note for anything else.
WHAT HAPPENS NEXT: every active site admin gets a bell notification and a
browser push the moment a report lands. The queue lives at Admin -> Moderation,
which now carries an open count in the tab label so you can see there is
something waiting from anywhere in the panel. Each row shows what was reported,
who wrote it, who reported it, the reason, a quote of the content, the room if
there was one, and a direct link to go look at it yourself. Close it as
RESOLVED (you acted) or DISMISSED (you looked, nothing wrong), with an optional
note. Either way the row stays, folded into a "Closed reports" section, so
there is a permanent record of what was reviewed and by whom.
Guards: you cannot report your own content, the same person reporting the same
thing twice inside a day does not create a second row, ten reports per member
per hour, guests cannot file at all, and reports.json is capped at 4,000 rows
with anything still open kept ahead of anything already closed.
This is a separate queue from the existing anti-spam holding pen. That one asks
"should this first post be published". This one asks "someone says this breaks
the rules". They are different questions and they now have different lists.
v9.5.9
The Canvas studio stops showing every layer twice
The studio was drawing your scene on top of a picture of your scene.
The stage you arrange things on is already live - every layer is a real element
sitting there. Underneath it, the studio parked the finished composite picture
as a backdrop, so that you could see your camera behind what you were building.
But that composite contains the layers too, so each one was on screen twice:
once as the thing you drag around, and once painted into the picture behind it.
It was never spotted because until the last release the composite drew text
tiny, white and in the wrong corner - it looked like noise, not a duplicate.
The moment that was fixed the two copies lined up and everything looked
doubled.
What sits behind the stage now is just your camera, which is the only thing the
stage was actually missing. What you look at is camera plus layers, which is
exactly what the room receives. The composite itself carries on being built
off-screen for the broadcast and is unchanged - the room was never seeing
double, only the studio was.
One detail worth knowing: the camera behind the stage is now stretched to the
stage exactly the way the broadcast stretches it, rather than being cropped to
fit. If the preview used to crop your camera slightly differently from what
went out, it no longer does - the preview exists to stop Apply being a
surprise, and a different crop is a surprise.
The stage you arrange things on is already live - every layer is a real element
sitting there. Underneath it, the studio parked the finished composite picture
as a backdrop, so that you could see your camera behind what you were building.
But that composite contains the layers too, so each one was on screen twice:
once as the thing you drag around, and once painted into the picture behind it.
It was never spotted because until the last release the composite drew text
tiny, white and in the wrong corner - it looked like noise, not a duplicate.
The moment that was fixed the two copies lined up and everything looked
doubled.
What sits behind the stage now is just your camera, which is the only thing the
stage was actually missing. What you look at is camera plus layers, which is
exactly what the room receives. The composite itself carries on being built
off-screen for the broadcast and is unchanged - the room was never seeing
double, only the studio was.
One detail worth knowing: the camera behind the stage is now stretched to the
stage exactly the way the broadcast stretches it, rather than being cropped to
fit. If the preview used to crop your camera slightly differently from what
went out, it no longer does - the preview exists to stop Apply being a
surprise, and a different crop is a surprise.
v9.5.8
Canvas layers go out on cam the way you built them
Text was still coming out tiny and unstyled after 9.5.7, and this is why: the
part that draws your scene onto your camera was reading the wrong element.
Every layer on the stage sits inside a positioning box, and none of how a layer
LOOKS lives on that box - the font size, colour, weight, alignment and padding
are all one level further in, on the layer's own element. The compositor was
reading the outer box, so it got the app's own inherited defaults instead of
yours: 16 pixels, plain white, no alignment, no padding. Measured in a real
browser, a 48-pixel red bold centred heading went out as 45-pixel plain white
left-aligned text - the small, unstyled result people were seeing.
Reading the outer box had two more consequences:
* It picked up every scrap of text inside that box, including the studio's
own placeholder labels. An image source that had not been given a picture
yet was broadcasting the words "Image Source" and "Double-click or open
Settings to set Image URL" into the room as if they were part of the scene.
* Emoji layers were drawn at 16 pixels in the top-left corner of their own
layer instead of at their real size in the middle of it, because the
centring is on the inner element too.
* Colour blocks drew nothing at all. A colour block paints its colour on its
own element, and the outer box is transparent, so the compositor looked at
the transparent one and moved on.
All four are fixed together by reading the layer's own element. Text now goes
out at your size, in your colour and weight, aligned where you put it, with
your line breaks and padding intact. Emoji sit at their real size, centred.
Colour blocks fill. Placeholder labels stay in the studio where they belong.
This was found and proved in a real browser driving the real app, not against a
stand-in for it - a text layer, an emoji layer and a colour block were put on
the stage and what the room would have received was measured for each.
part that draws your scene onto your camera was reading the wrong element.
Every layer on the stage sits inside a positioning box, and none of how a layer
LOOKS lives on that box - the font size, colour, weight, alignment and padding
are all one level further in, on the layer's own element. The compositor was
reading the outer box, so it got the app's own inherited defaults instead of
yours: 16 pixels, plain white, no alignment, no padding. Measured in a real
browser, a 48-pixel red bold centred heading went out as 45-pixel plain white
left-aligned text - the small, unstyled result people were seeing.
Reading the outer box had two more consequences:
* It picked up every scrap of text inside that box, including the studio's
own placeholder labels. An image source that had not been given a picture
yet was broadcasting the words "Image Source" and "Double-click or open
Settings to set Image URL" into the room as if they were part of the scene.
* Emoji layers were drawn at 16 pixels in the top-left corner of their own
layer instead of at their real size in the middle of it, because the
centring is on the inner element too.
* Colour blocks drew nothing at all. A colour block paints its colour on its
own element, and the outer box is transparent, so the compositor looked at
the transparent one and moved on.
All four are fixed together by reading the layer's own element. Text now goes
out at your size, in your colour and weight, aligned where you put it, with
your line breaks and padding intact. Emoji sit at their real size, centred.
Colour blocks fill. Placeholder labels stay in the studio where they belong.
This was found and proved in a real browser driving the real app, not against a
stand-in for it - a text layer, an emoji layer and a colour block were put on
the stage and what the room would have received was measured for each.
v9.5.7
Canvas text works, Canvas remembers your workspace, and store item icons finally show up
CANVAS TEXT. A Text source never made it into the room. What went out was the
literal words "Hello World" in plain white, no matter what you typed, because
the part that draws the scene onto your camera was reading two fields the app
has never written - it wanted "textContent" while the editor and the on-screen
preview both use "content". It also handed the layer width to the browser as a
squeeze instead of a wrap, so anything long came out crushed onto one line.
Text now goes out exactly as the studio shows it: your words, your font, your
colour and size, real wrapping, line breaks kept, left/centre/right honoured,
and a word wrapped in **stars** comes out bold.
The alphabets behind the Text Style menu were damaged too. Ten of the twenty
had English words spliced into them - "block", "External", "content", "key",
"Configuration" - plus a stray "<i>", some ASCII dots and a couple of flag
characters where letters should have been. In Bold Serif that meant B came out
as a small b, C as an l, D as an o. Every alphabet is now generated from the
Unicode standard rather than typed by hand, so it cannot drift again, and each
option's preview text in the dropdown is produced by the option itself, so a
label can never advertise something the option does not do. Separately, the
Bold font had lost W entirely and was giving V the W glyph.
CANVAS REMEMBERS YOUR WORKSPACE. Scenes lived in the browser's memory and
nowhere else - close the tab and the whole workspace was gone, and the only way
to keep one was to export a file by hand and import it again next time. Your
scenes, sources, layers, hotkeys and performance settings now save to your
account as you work and come back the next time you open the studio, the same
way your cam Effects already follow you around. There is a strip under the top
bar with Save, Restore and Clear, and it saves itself in the background while
you build.
Your save is private to your account. It is never listed, never shown to
another member, and there is no way to load someone else's - which is
deliberate, because a Code source is real JavaScript and the one thing that
keeps it safe is that it can only ever run in the browser of the person who
wrote it. A picture pasted into a scene is stored inside that scene, so a
workspace stuffed with pasted images can outgrow the save limit; if that
happens the studio says so and tells you to use an image URL instead. Guests
see no save strip at all, since there is no account to save to.
STORE ITEM ICONS. Uploading an icon image for a store item has never worked.
Two reasons, both silent: nothing created the folder the icon was meant to go
in, so the upload failed and the store swallowed the failure, and even if the
folder had existed the address handed back pointed at the wrong place, so the
picture would have 404'd anyway. Both are fixed, and item screenshots - which
went through the same path - are fixed with them.
Typed emoji icons were being cut in half. Anything made of more than two pieces
lost the rest, which is most modern emoji: a skin tone was dropped, a rainbow
flag lost its rainbow, and 👨💻 came out as a man with an invisible joiner stuck
to him. The field also refused to accept those emoji in the first place. Both
fixed; a whole emoji now goes in and comes back out whole.
15 MORE ITEMS IN THE STORE, ready to sell on day one: five new room entrance
sounds (Level Up, Coin Drop, Soft Knock, Laser Zap, Water Droplet), three name
effects (Neon Ice, Molten, Deep Sea), two cursors (Vinyl, Paintbrush), two
hover effects (Card Sheen, Soft Focus), two screen overlays (Dust Motes, Aurora
Veil) and a Music Note mouse trail. The store now ships with 132 items and adds
new ones to an existing site by itself - you do not re-import anything, and
nothing you have edited or repriced is touched.
ALSO: the buy page, the member FAQ and the admin guide now cover what the site
actually does - the cam-room connection work from 9.5.6, per-member timestamps,
the Link readout on the right-click menu, the Cams tab and its relay-promotion
lines, item icons, and Creative Canvas, none of which were written down
anywhere.
literal words "Hello World" in plain white, no matter what you typed, because
the part that draws the scene onto your camera was reading two fields the app
has never written - it wanted "textContent" while the editor and the on-screen
preview both use "content". It also handed the layer width to the browser as a
squeeze instead of a wrap, so anything long came out crushed onto one line.
Text now goes out exactly as the studio shows it: your words, your font, your
colour and size, real wrapping, line breaks kept, left/centre/right honoured,
and a word wrapped in **stars** comes out bold.
The alphabets behind the Text Style menu were damaged too. Ten of the twenty
had English words spliced into them - "block", "External", "content", "key",
"Configuration" - plus a stray "<i>", some ASCII dots and a couple of flag
characters where letters should have been. In Bold Serif that meant B came out
as a small b, C as an l, D as an o. Every alphabet is now generated from the
Unicode standard rather than typed by hand, so it cannot drift again, and each
option's preview text in the dropdown is produced by the option itself, so a
label can never advertise something the option does not do. Separately, the
Bold font had lost W entirely and was giving V the W glyph.
CANVAS REMEMBERS YOUR WORKSPACE. Scenes lived in the browser's memory and
nowhere else - close the tab and the whole workspace was gone, and the only way
to keep one was to export a file by hand and import it again next time. Your
scenes, sources, layers, hotkeys and performance settings now save to your
account as you work and come back the next time you open the studio, the same
way your cam Effects already follow you around. There is a strip under the top
bar with Save, Restore and Clear, and it saves itself in the background while
you build.
Your save is private to your account. It is never listed, never shown to
another member, and there is no way to load someone else's - which is
deliberate, because a Code source is real JavaScript and the one thing that
keeps it safe is that it can only ever run in the browser of the person who
wrote it. A picture pasted into a scene is stored inside that scene, so a
workspace stuffed with pasted images can outgrow the save limit; if that
happens the studio says so and tells you to use an image URL instead. Guests
see no save strip at all, since there is no account to save to.
STORE ITEM ICONS. Uploading an icon image for a store item has never worked.
Two reasons, both silent: nothing created the folder the icon was meant to go
in, so the upload failed and the store swallowed the failure, and even if the
folder had existed the address handed back pointed at the wrong place, so the
picture would have 404'd anyway. Both are fixed, and item screenshots - which
went through the same path - are fixed with them.
Typed emoji icons were being cut in half. Anything made of more than two pieces
lost the rest, which is most modern emoji: a skin tone was dropped, a rainbow
flag lost its rainbow, and 👨💻 came out as a man with an invisible joiner stuck
to him. The field also refused to accept those emoji in the first place. Both
fixed; a whole emoji now goes in and comes back out whole.
15 MORE ITEMS IN THE STORE, ready to sell on day one: five new room entrance
sounds (Level Up, Coin Drop, Soft Knock, Laser Zap, Water Droplet), three name
effects (Neon Ice, Molten, Deep Sea), two cursors (Vinyl, Paintbrush), two
hover effects (Card Sheen, Soft Focus), two screen overlays (Dust Motes, Aurora
Veil) and a Music Note mouse trail. The store now ships with 132 items and adds
new ones to an existing site by itself - you do not re-import anything, and
nothing you have edited or repriced is touched.
ALSO: the buy page, the member FAQ and the admin guide now cover what the site
actually does - the cam-room connection work from 9.5.6, per-member timestamps,
the Link readout on the right-click menu, the Cams tab and its relay-promotion
lines, item icons, and Creative Canvas, none of which were written down
anywhere.
v9.5.6
Cams that hold on to a bad wifi link, and room clocks that read in your own time zone
FLAKY WIFI. A camera on a wifi link that keeps dropping and coming back is a
different problem from one that never connects, and until now the room treated
them the same. Every counter that decides what to do next was reset the instant
the connection came back, so a link that was up for two seconds every twenty
looked like a peer that kept recovering. It never escalated, and it reconnected
the same failing way for as long as the person stayed in the room. Three things
change:
* The recovery budget is now earned back by a connection that HOLDS, not one
that merely happens. A drop after a good run costs nothing, exactly as
before. A drop that ends a connection which never settled is counted.
* After three of those, that person's cam is moved onto the TURN relay for
the rest of their visit. A relay path is the one thing that survives the
two events that actually cause this - a phone or laptop roaming to another
wifi access point, and a router dropping an idle connection - because both
change the network address underneath a live call and re-trying the direct
path just loses it again a minute later. It shows up in the cam log under
Admin, by name, so you can see who it happened to.
* If YOUR OWN network drops out entirely, the room now waits instead of
spending its recovery attempts on a dead connection. A twenty-five second
wifi dropout used to use up every attempt and then remove every other
person's cam from your screen - and roughly half of them did not come back
on their own. It also now notices a phone handing wifi over to cellular,
which does not register as going offline and so used to sit for twelve
seconds before anything reacted.
UPLOAD THAT BACKS OFF FOR THE RIGHT REASON. The quality governor added in 9.2.3
asks the browser what is limiting the encoder, which answers "is the device
struggling" and "is the line too slow" but not "are the packets surviving the
trip". On wifi with interference the honest answer to the first two is no, and
the picture still arrives smeared and blocky. It now also reads how much is
being lost in transit, and eases the upload when a sustained eighth of it is
not arriving. Below that it does nothing, because every ordinary connection
loses the odd packet all day. And when a bad link forces the upload down, that
is remembered for the visit: a stalled camera used to rebuild its connection and
climb straight back to full quality, flood the same link, and stall again. The
memory relaxes on its own as soon as the connection is genuinely clear again.
LINK HEALTH, WHERE THE SPEED READING ALREADY LIVES. Right-clicking someone's cam
already showed their live speed. It now also shows how steady the link is -
packet loss, how many times they have dropped, and whether they are on the
relay - because a cam sitting at a healthy speed while losing a tenth of its
packets reads fine on a speed meter and looks like a slideshow on screen. Your
own menu shows the same for your upload, plus what the governor is currently
doing about it. Nothing new appears on the cams themselves.
ROOM CLOCKS READ IN YOUR TIME ZONE. Every timestamp in every chat room came
from the site-wide time zone setting, so a member three time zones away read
the room in someone else's local time with no way to change it - even though
the profile page has offered a personal time zone for a long time and says out
loud that it is used for time displays around the site. Room timestamps now
follow it: the zone you picked if you picked one, otherwise your device's own
zone, which is what "Auto-detect from my browser" on that page has always
promised. The messages already on screen when you walk in are re-stamped to
match, so a room never shows two different clocks. If you have never touched
that setting and your browser will not say where it is, the room reads exactly
as it did before.
different problem from one that never connects, and until now the room treated
them the same. Every counter that decides what to do next was reset the instant
the connection came back, so a link that was up for two seconds every twenty
looked like a peer that kept recovering. It never escalated, and it reconnected
the same failing way for as long as the person stayed in the room. Three things
change:
* The recovery budget is now earned back by a connection that HOLDS, not one
that merely happens. A drop after a good run costs nothing, exactly as
before. A drop that ends a connection which never settled is counted.
* After three of those, that person's cam is moved onto the TURN relay for
the rest of their visit. A relay path is the one thing that survives the
two events that actually cause this - a phone or laptop roaming to another
wifi access point, and a router dropping an idle connection - because both
change the network address underneath a live call and re-trying the direct
path just loses it again a minute later. It shows up in the cam log under
Admin, by name, so you can see who it happened to.
* If YOUR OWN network drops out entirely, the room now waits instead of
spending its recovery attempts on a dead connection. A twenty-five second
wifi dropout used to use up every attempt and then remove every other
person's cam from your screen - and roughly half of them did not come back
on their own. It also now notices a phone handing wifi over to cellular,
which does not register as going offline and so used to sit for twelve
seconds before anything reacted.
UPLOAD THAT BACKS OFF FOR THE RIGHT REASON. The quality governor added in 9.2.3
asks the browser what is limiting the encoder, which answers "is the device
struggling" and "is the line too slow" but not "are the packets surviving the
trip". On wifi with interference the honest answer to the first two is no, and
the picture still arrives smeared and blocky. It now also reads how much is
being lost in transit, and eases the upload when a sustained eighth of it is
not arriving. Below that it does nothing, because every ordinary connection
loses the odd packet all day. And when a bad link forces the upload down, that
is remembered for the visit: a stalled camera used to rebuild its connection and
climb straight back to full quality, flood the same link, and stall again. The
memory relaxes on its own as soon as the connection is genuinely clear again.
LINK HEALTH, WHERE THE SPEED READING ALREADY LIVES. Right-clicking someone's cam
already showed their live speed. It now also shows how steady the link is -
packet loss, how many times they have dropped, and whether they are on the
relay - because a cam sitting at a healthy speed while losing a tenth of its
packets reads fine on a speed meter and looks like a slideshow on screen. Your
own menu shows the same for your upload, plus what the governor is currently
doing about it. Nothing new appears on the cams themselves.
ROOM CLOCKS READ IN YOUR TIME ZONE. Every timestamp in every chat room came
from the site-wide time zone setting, so a member three time zones away read
the room in someone else's local time with no way to change it - even though
the profile page has offered a personal time zone for a long time and says out
loud that it is used for time displays around the site. Room timestamps now
follow it: the zone you picked if you picked one, otherwise your device's own
zone, which is what "Auto-detect from my browser" on that page has always
promised. The messages already on screen when you walk in are re-stamped to
match, so a room never shows two different clocks. If you have never touched
that setting and your browser will not say where it is, the room reads exactly
as it did before.
v9.5.5
Creative Canvas, 1,000+ TV channels, and a rebuild that cannot take the site down
CREATIVE CANVAS - a scene compositor that layers over your camera, free and
included with every edition. Put pictures, video, text, drawings, colour blocks
and emoji on top of your cam, arrange them in layers with filters, chroma key
and shapes, save the arrangement as a scene, and go live with the whole thing
as your camera in any cam room. It works the way Effects does: your camera is
the bottom layer, the scene is painted over it onto a canvas, and that canvas
becomes what the room sees. No screen sharing and no extra permission prompt.
There is a Canvas button on the room tools bar, next to Share and Effects. Cam
up the way you always do, then press it. The studio opens on a LIVE PREVIEW of
your own camera with the scene over it, while the room carries on seeing your
plain camera - nothing changes for anyone else until you say so. Arrange the
scene against your real picture, press Apply, and the studio steps aside and
puts you back in the room with the scene now on your cam. Remove scene puts
your plain camera back. Pressing Apply again while a scene is running updates
it in place.
A note for anyone who tried this on their own: swapping the outbound video
counted the mesh peers and the SFU publisher but not the local stream itself,
so with nobody else in the room it reported "nothing was sending" and refused
a swap that had in fact worked. Being alone in a room is a normal state, not a
failed swap - it counts now, and Creative Canvas confirms an Apply by reading
back what is actually outbound rather than trusting a tally. Scenes, pictures and any code you write are stored in
your own browser and are never uploaded; what leaves is a video track.
One limit is worth knowing up front: embedded sources - YouTube, Twitch, web
pages and Code widgets - cannot be drawn onto a video track, because a browser
will not let a page read the pixels inside someone else's embedded page. Those
layers are left out of the broadcast and the studio tells you how many were
skipped rather than letting them quietly disappear. Everything else goes out.
Original studio by Jordan Albiar.
OHSTV - the channel list went from 454 to 1,014. Twenty-one channels were
removed: fifteen no longer exist in any upstream source and six were running on
unlicensed bare-IP restreams, which is where most of the rot came from.
Twenty-six more had stale stream URLs, now refreshed. Five hundred and
eighty-five channels were added from Pluto TV, Samsung TV Plus and Plex only -
official free services with the same reliability as the ones already in the
list - and heavy on 24/7 single-show channels. A shipped channel list now
reaches sites that have already been running: it merges once, refreshing URLs,
adding what is new and dropping what is gone, while leaving channels you added
or edited by hand completely alone. "Refresh from source" also flags any
channel that has vanished from every source, guarded so a failed source fetch
can never mark a whole category dead.
LITE HAS NO CAMS, AND NOW LOOKS LIKE IT. Cam FX was still linked from the
logged-out navigation and from the footer of every page, on an edition with no
webcams at all - so a Lite site advertised webcam effects it could not run.
Both links are gated now, typing ?page=camfx or ?page=canvas lands on the home
page instead of a dead end, Creative Canvas does not load on a no-cam edition
at all, and the Lite download no longer contains an ohs_cam_uploads folder or
an install step telling you to make it writable. Paid editions are unchanged.
KLOUT moved off the main navigation bar into the Community menu, alongside
Achievements, Lottery and Friends, where the other member-standing pages live.
BUYER PACKAGE REBUILD - the automatic rebuild that runs after a deploy could
throw an uncaught error on an ordinary page load, taking the site down for
every visitor until the next upload. The cause was a helper declared inside a
conditional, which PHP does not hoist, so it did not exist yet at the point the
rebuild called it; it only surfaced when buyer_assets/CHANGELOG.txt was absent,
which is why it looked intermittent. Fixed, and the rebuild is now wrapped so
that any future failure is recorded in the Build tab's report instead of
thrown at whoever happens to load a page.
included with every edition. Put pictures, video, text, drawings, colour blocks
and emoji on top of your cam, arrange them in layers with filters, chroma key
and shapes, save the arrangement as a scene, and go live with the whole thing
as your camera in any cam room. It works the way Effects does: your camera is
the bottom layer, the scene is painted over it onto a canvas, and that canvas
becomes what the room sees. No screen sharing and no extra permission prompt.
There is a Canvas button on the room tools bar, next to Share and Effects. Cam
up the way you always do, then press it. The studio opens on a LIVE PREVIEW of
your own camera with the scene over it, while the room carries on seeing your
plain camera - nothing changes for anyone else until you say so. Arrange the
scene against your real picture, press Apply, and the studio steps aside and
puts you back in the room with the scene now on your cam. Remove scene puts
your plain camera back. Pressing Apply again while a scene is running updates
it in place.
A note for anyone who tried this on their own: swapping the outbound video
counted the mesh peers and the SFU publisher but not the local stream itself,
so with nobody else in the room it reported "nothing was sending" and refused
a swap that had in fact worked. Being alone in a room is a normal state, not a
failed swap - it counts now, and Creative Canvas confirms an Apply by reading
back what is actually outbound rather than trusting a tally. Scenes, pictures and any code you write are stored in
your own browser and are never uploaded; what leaves is a video track.
One limit is worth knowing up front: embedded sources - YouTube, Twitch, web
pages and Code widgets - cannot be drawn onto a video track, because a browser
will not let a page read the pixels inside someone else's embedded page. Those
layers are left out of the broadcast and the studio tells you how many were
skipped rather than letting them quietly disappear. Everything else goes out.
Original studio by Jordan Albiar.
OHSTV - the channel list went from 454 to 1,014. Twenty-one channels were
removed: fifteen no longer exist in any upstream source and six were running on
unlicensed bare-IP restreams, which is where most of the rot came from.
Twenty-six more had stale stream URLs, now refreshed. Five hundred and
eighty-five channels were added from Pluto TV, Samsung TV Plus and Plex only -
official free services with the same reliability as the ones already in the
list - and heavy on 24/7 single-show channels. A shipped channel list now
reaches sites that have already been running: it merges once, refreshing URLs,
adding what is new and dropping what is gone, while leaving channels you added
or edited by hand completely alone. "Refresh from source" also flags any
channel that has vanished from every source, guarded so a failed source fetch
can never mark a whole category dead.
LITE HAS NO CAMS, AND NOW LOOKS LIKE IT. Cam FX was still linked from the
logged-out navigation and from the footer of every page, on an edition with no
webcams at all - so a Lite site advertised webcam effects it could not run.
Both links are gated now, typing ?page=camfx or ?page=canvas lands on the home
page instead of a dead end, Creative Canvas does not load on a no-cam edition
at all, and the Lite download no longer contains an ohs_cam_uploads folder or
an install step telling you to make it writable. Paid editions are unchanged.
KLOUT moved off the main navigation bar into the Community menu, alongside
Achievements, Lottery and Friends, where the other member-standing pages live.
BUYER PACKAGE REBUILD - the automatic rebuild that runs after a deploy could
throw an uncaught error on an ordinary page load, taking the site down for
every visitor until the next upload. The cause was a helper declared inside a
conditional, which PHP does not hoist, so it did not exist yet at the point the
rebuild called it; it only surfaced when buyer_assets/CHANGELOG.txt was absent,
which is why it looked intermittent. Fixed, and the rebuild is now wrapped so
that any future failure is recorded in the Build tab's report instead of
thrown at whoever happens to load a page.
v9.5.3
Lite stops advertising the cam features it does not have
LITE HAS NO CAMS AND IT WAS STILL SELLING WEBCAM TOOLS. Cam FX is an effects
add-on for a cam room and Creative Canvas draws a scene over a camera, so on an
edition built without cams both are dead ends - and a link to one tells a Lite
buyer they are missing something they were never sold. The logged-in nav had
been gated already; the site footer and the add-on page had not, which is how
Cam FX kept surfacing on Lite builds. OHS_CAMS_OK is the single gate for
anything cam-related now, and it covers the nav, the footer and the pages.
AND THE INSTALLER STOPPED ASKING FOR A FOLDER LITE NEVER USES. A no-cams build
no longer ships an ohs_cam_uploads directory, and the setup steps no longer tell
the buyer to make it writable and wonder what they are missing.
v9.5.2
Creative Canvas is reachable before you go live
BUILDING A SCENE BEFORE YOU CAM UP IS THE NORMAL WAY TO USE IT. The Canvas
button sat in the group enableControls() switches on, and that only runs once
the peer handshake has completed - that is, once you are already broadcasting.
So the button was dead for exactly the people most likely to want it, and for
every watcher. It ships enabled now, and the Go live step checks for an outbound
track itself and says so plainly when there is not one.
A ROOM SCRIPT'S TOP-LEVEL let IS NOT A WINDOW PROPERTY. localStream and peers
are lexical bindings, so a separately loaded script - a mod's asset, an add-on -
could not see them at all, and window.localStream read back undefined. The
outbound video track is reachable through a proper accessor now, which is what
lets an add-on find the live track instead of guessing at it.
v9.5.1
A failed package rebuild can no longer take the site down
THE REBUILD STEP RUNS FROM THE TOP OF AN ORDINARY REQUEST. Anything thrown
inside the builder therefore became an uncaught Error on a page every visitor
loads - and 9.5.0 shipped exactly that: the builder calls a helper declared
inside a function_exists() guard further down the file, so at the point the
builder runs it does not exist yet. The old claim that "every function the
builder calls is already declared" holds only for unconditional top-level
declarations, which that one is not.
The builder runs inside a try/catch now, and a failure is recorded in the same
last-build report the Build tab already shows. A broken build is something you
read on the Build tab instead of something your visitors find first.
Also here: the release history moved into one place at ?page=changelog, and the
cam picker learned to accept a ready-made stream - screen share today - keying
on the stream being present rather than on one hard-coded kind, so another
source of the same shape needs no further edit. It passes the content hint with
it, which is what stops an encoder treating captured video as graphics and
holding the resolution while it drops the frame rate.
v9.5.0
Public release-history page
A full, readable release history at ?page=changelog - every version, what
changed and why. Linked from the buy page and from Admin -> Settings. Reads
buyer_assets/CHANGELOG.txt when present so the live text is yours to edit, and
falls back to the built-in history otherwise. Version index down the side,
deep links to any release.
changed and why. Linked from the buy page and from Admin -> Settings. Reads
buyer_assets/CHANGELOG.txt when present so the live text is yours to edit, and
falls back to the built-in history otherwise. Version index down the side,
deep links to any release.
v9.4.9
Security hardening pass
Download tokens, licence keys and 2FA codes are now compared in constant time
(hash_equals) instead of ===, so no comparison leaks how much of a secret
matched. The two unauthenticated download-token endpoints are rate limited -
they take a token straight off the query string with no session behind them,
so constant-time comparison stops the timing leak and the limiter stops the
guessing. TOTP verification gained an attempt limit: a six-digit code is a
million guesses, which is a weekend for a script. "Export for Store" in the
Theme Builder had shipped DEAD - the button linked to an action with no
handler anywhere, so clicking it fell through to the homepage; it now exports.
And all four theme writers check whether the write actually succeeded instead
of reporting "Saved!" over a file that never changed.
(hash_equals) instead of ===, so no comparison leaks how much of a secret
matched. The two unauthenticated download-token endpoints are rate limited -
they take a token straight off the query string with no session behind them,
so constant-time comparison stops the timing leak and the limiter stops the
guessing. TOTP verification gained an attempt limit: a six-digit code is a
million guesses, which is a weekend for a script. "Export for Store" in the
Theme Builder had shipped DEAD - the button linked to an action with no
handler anywhere, so clicking it fell through to the homepage; it now exports.
And all four theme writers check whether the write actually succeeded instead
of reporting "Saved!" over a file that never changed.
v9.4.8
Front page: one editable page, and icons that cannot go missing
The lower feature strip is now a single page in the Page Editor alongside
About / Terms / Privacy - heading, intro and all six tiles in one body, edited
in one place. The tiles used raw emoji, which is a font lookup: a machine
whose font lacks the character draws an empty box. They are now CSS-masked
artwork, which no font can fail to supply. Inline SVG was tried first and
rejected - the page sanitiser strips <svg> as stored-XSS defence, so every
icon would have vanished the first time the page was saved. The sanitiser was
not weakened.
About / Terms / Privacy - heading, intro and all six tiles in one body, edited
in one place. The tiles used raw emoji, which is a font lookup: a machine
whose font lacks the character draws an empty box. They are now CSS-masked
artwork, which no font can fail to supply. Inline SVG was tried first and
rejected - the page sanitiser strips <svg> as stored-XSS defence, so every
icon would have vanished the first time the page was saved. The sanitiser was
not weakened.
v9.4.7
Cam FX keeps running when you tab away
requestVideoFrameCallback and requestAnimationFrame are both driven by the
browser's rendering steps, which do not run for a tab nobody is looking at -
so on Firefox and Waterfox the outgoing camera stopped dead when you switched
tab and the room watched a still photo. A timer that is not tied to the
rendering steps now keeps the beat while hidden. Measured: 0 frames delivered
across four hidden seconds before, 80 after. Nothing new runs while the tab is
visible.
browser's rendering steps, which do not run for a tab nobody is looking at -
so on Firefox and Waterfox the outgoing camera stopped dead when you switched
tab and the room watched a still photo. A timer that is not tied to the
rendering steps now keeps the beat while hidden. Measured: 0 frames delivered
across four hidden seconds before, 80 after. Nothing new runs while the tab is
visible.
v9.4.6
Cam FX frame path restored
Four releases of changes to the camera pipeline, argued from mechanism rather
than measurement, cost frame rate rather than saving it. The whole frame path
was taken back to the last known-good version wholesale and verified byte for
byte against it.
than measurement, cost frame rate rather than saving it. The whole frame path
was taken back to the last known-good version wholesale and verified byte for
byte against it.
v9.4.3
SFU stops discarding working sessions on no evidence
A poll that answered with an error, or with no roster at all, was
indistinguishable from an empty room - and both tore down a working SFU
session, which unpublishes you and kills every viewer's stream. A roster must
now be usable before it counts. Hysteresis also needed more than a clock: the
poll cadence runs from 2s bursts to a 20s cruise, so "quiet for 25 seconds"
could be two readings. Four separate usable readings are now required. Four
presence writers that never received the 9.1.6 lock - cam_leave, guest delete,
kick-all-guests, and a read path that was quietly writing - now go through it.
indistinguishable from an empty room - and both tore down a working SFU
session, which unpublishes you and kills every viewer's stream. A roster must
now be usable before it counts. Hysteresis also needed more than a clock: the
poll cadence runs from 2s bursts to a 20s cruise, so "quiet for 25 seconds"
could be two readings. Four separate usable readings are now required. Four
presence writers that never received the 9.1.6 lock - cam_leave, guest delete,
kick-all-guests, and a read path that was quietly writing - now go through it.
v9.4.2
The Cam FX governor stops grading itself on readings it could not take
A MEASUREMENT YOU COULD NOT TAKE IS NOT A MEASUREMENT OF ZERO. The quality
governor treated an unavailable reading exactly like a bad one, so it answered
its own blind spots by giving up picture quality - and the busier the page got,
the less reliable the readings became, which made it give up more. An unusable
sample now leaves every counter where it was instead of counting against the
picture.
v9.4.1
SFU: every judgement now expires with the session it was about
EVERY LEDGER IN THIS MODULE IS A JUDGEMENT ABOUT ONE SESSION, and when that
session goes away the judgement has to go with it. It did not. A peer who
republished - who had, in other words, just fixed themselves - was still
carrying the freeze counts, re-pull budgets and hand-off marks earned by a
stream that no longer existed, and was judged on them. Those ledgers are cleared
the moment the session they describe is gone. What we know about WHO a peer is
deliberately survives: a name is not a verdict, and it has to outlive their tile
or the watchdog churns them the moment they are dropped.
A MESH HAND-OFF IS A LOAN, NOT A LIFE SENTENCE. A peer released to a direct
connection was marked so for the rest of the visit - exiled at 18:34, still
exiled at 19:03, long after they had rebuilt their leg. Hand-offs expire on
their own now, and are cleared outright the moment that peer republishes. There
is also a ceiling on how many we hand out at once, because a direct connection
is not free for the person we hand off: mesh makes them encode and upload once
per viewer instead of once in total, so a room where every viewer exiles every
weak publisher rebuilds the exact upload explosion the SFU exists to prevent,
and it lands hardest on the devices that already said they could not keep up.
Past the cap we stay on the SFU and back off, which costs one stalled tile
rather than somebody else's whole uplink.
IS ANYTHING STILL ARRIVING? framesDecoded on its own cannot tell a dead forward
leg from a decoder stuck waiting for a keyframe, and those two need opposite
answers. The watchdog reads whether bytes and packets moved while the picture
did not, and a stuck decoder gets the free rung: re-attaching the video source
rebuilds the decode pipeline with no renegotiation, no request upstream and no
tile teardown. That rung has its own budget, bought back by real video, because
a cheap rung with no ceiling is how every rung above it becomes unreachable -
the "(attempt 1)" shape this module has been bitten by more than once.
A REPUBLISH VOIDS THE REPORTS THAT CAUSED IT. In the live log a peer republished
because two peers froze at once, then republished again seconds later on a
report banked against the leg it had just replaced. A viewer whose watchdog
fired against a session we have already thrown away is describing the past;
their next freeze, if there is one, arrives on the new leg and counts. A leg
rebuilt seconds ago is given time to settle before any detector may judge it -
the cooldowns pace the ACTION, this paces the EVIDENCE.
AND THE PUBLISHER'S ADVERTISEMENT IS REPAIRED IN PLACE. A presence row that
survived but lost its sfu_session was answered with "you are gone", after which
the client had to notice, wait out its own rate limit, and spend a whole extra
request filing the value the first one was already carrying. For that entire
window every other client's roster said this publisher had nothing to pull, and
released their tracks. The heartbeat already carries the session id, so it goes
back on the same request.
v9.4.0
Cam FX download counter
Counted server-side at the moment bytes are handed over, across all three
builds. Userscript managers re-read the update URL daily and that request is
indistinguishable from an install, so metadata moved to its own endpoint. HEAD
requests, resumed ranges and repeat clicks within 30 minutes are not counted;
crawlers are counted separately. The dedupe key is a salted hash with a daily-
rolling salt, so it cannot be reversed. Concurrency measured at 100
simultaneous downloads: no lock counted 32, locking the data file counted 86,
a sentinel lock file counted 100.
builds. Userscript managers re-read the update URL daily and that request is
indistinguishable from an install, so metadata moved to its own endpoint. HEAD
requests, resumed ranges and repeat clicks within 30 minutes are not counted;
crawlers are counted separately. The dedupe key is a salted hash with a daily-
rolling salt, so it cannot be reversed. Concurrency measured at 100
simultaneous downloads: no lock counted 32, locking the data file counted 86,
a sentinel lock file counted 100.
v9.3.9
Cam FX moved off the main thread
At 5fps in a busy chat room the work was never expensive - it was queueing
behind the site's own JavaScript. The pipeline now runs entirely in a worker:
read the track, paint on an offscreen canvas, write the result. Measured 16.8
-> 30.6fps with the page's main thread pegged. The quality governor now tells
starvation from overload, so it stops giving up resolution for a problem
resolution cannot fix.
behind the site's own JavaScript. The pipeline now runs entirely in a worker:
read the track, paint on an offscreen canvas, write the result. Measured 16.8
-> 30.6fps with the page's main thread pegged. The quality governor now tells
starvation from overload, so it stops giving up resolution for a problem
resolution cannot fix.
v9.3.8
Cam FX became an exact mirror of the site's engine
Effects looked wrong off-site because the site's are CSS on a small tile and
CSS pixels are relative to that box - a 4px frame is 1% of a 360px tile and a
quarter of that on a 1280px canvas. Everything now scales from a reference
width. The frame styles had been written from impressions rather than from the
stylesheet and were rewritten against it: retro draws hard 45-degree stripes,
chase rotates the whole ring, groove/ridge/inset split by side like a real CSS
bevel, film is rails on left and right with scrolling sprockets. The add-on no
longer runs on this site at all - the rooms have it built in and a second copy
was fighting the first.
CSS pixels are relative to that box - a 4px frame is 1% of a 360px tile and a
quarter of that on a 1280px canvas. Everything now scales from a reference
width. The frame styles had been written from impressions rather than from the
stylesheet and were rewritten against it: retro draws hard 45-degree stripes,
chase rotates the whole ring, groove/ridge/inset split by side like a real CSS
bevel, film is rails on left and right with scrolling sprockets. The add-on no
longer runs on this site at all - the rooms have it built in and a second copy
was fighting the first.
v9.3.3 - 9.3.7
Cam FX: full option parity and the lag hunt
Every option the site has, in all three add-on builds, including the image-
over-cam overlay. Then four rounds on latency: a video element in the middle
of the pipeline, rAF running at the monitor's refresh rate instead of the
camera's, a capture clock sampling on its own timer, a track that reported a
frame rate of zero and told other sites' encoders to expect no frames, and a
quality governor grading itself against its own failure so the worse it got
the better it thought it was doing.
over-cam overlay. Then four rounds on latency: a video element in the middle
of the pipeline, rAF running at the monitor's refresh rate instead of the
camera's, a capture clock sampling on its own timer, a track that reported a
frame rate of zero and told other sites' encoders to expect no frames, and a
quality governor grading itself against its own failure so the worse it got
the better it thought it was doing.
v9.3.1 - 9.3.2
Cam FX add-on, three builds
Chromium extension, Firefox add-on and userscript, all from one source. Cover
colours, virtual background and the legacy signal file.
colours, virtual background and the legacy signal file.
v9.1.3 - 9.3.0
Media Center, deploy guard, cover colours
A universal media player, an update guard on deploys, chat double-post fix,
picture-in-picture for cams.
picture-in-picture for cams.
v9.0.3 - 9.1.2
Ban ladder and SFU hardening
Escalating room bans, the Media Center, and a run of SFU reliability work.
v8.9.5 - 9.0.2
TURN, relays and connection reliability
An ordered relay provider list so a partial or junk setting can never lose
relays, a shared-secret relay path that computes credentials locally and
cannot fail at page load, and a self-test in the admin panel that reports
honestly when a relay returns nothing.
relays, a shared-secret relay path that computes credentials locally and
cannot fail at page load, and a self-test in the admin panel that reports
honestly when a relay returns nothing.
v8.7.9 - 8.9.4
Signal batching, impersonation, content pack
Cam signal coalescing, faster cam connect, admin log-in-as-user, capacity copy
and overlay fixes.
and overlay fixes.
v8.7.8
One owner for the profile banner
TWO SYSTEMS COULD STYLE THE SAME BANNER, AND BOTH DID. A store banner add-on and
the Banner Builder both target .ohs-banner. Every store declaration is forced
!important - including the background shorthand, which also pins the position -
and an !important declaration beats a CSS animation, so layering the two did not
merely look wrong, it silently killed the built banner's movement. Exactly one
system may style the banner now, and the member picks which: their built banner
by default, or the equipped store banner if they would rather it took over. When
the store banner wins, the builder's output is skipped entirely instead of being
emitted underneath it.
AND SAVING A PROFILE ON A STORE-LESS INSTALL STOPPED WIPING THE SETTINGS. The
whole banner group is gated on the store being present, so on an install without
it none of those fields are posted at all - and the save path read them
unconditionally, quietly clearing saved values on every profile save. The form
stamps a marker inside the gate; when that marker is absent, whatever is already
stored is kept.
v8.7.4 - 8.7.7
Security badges earned at runtime
Trust badges that are earned by a live self-check rather than asserted, a
security.txt, and a self-check probe.
security.txt, and a self-check probe.
v8.7.3
Cam effects: borders, overlays, image over cam
17 border styles, 16 animated overlays and an image-over-cam layer with size,
opacity, position and rotation. Built on the existing effects broadcast rather
than a canvas pipeline, so effects cost the broadcaster nothing: no re-encode,
no extra bandwidth, no renegotiation. Worst-case wire payload measured at 335
bytes.
opacity, position and rotation. Built on the existing effects broadcast rather
than a canvas pipeline, so effects cost the broadcaster nothing: no re-encode,
no extra bandwidth, no renegotiation. Worst-case wire payload measured at 335
bytes.
v8.6.8 - 8.7.2
Selective subscription and bigger rooms
The upload ceiling was already gone - one stream per publisher regardless of
room size - so the real limit was the download side, where every client
decodes every stream it pulls. Selective subscription caps how many remote
peers any one client subscribes to, which is the cost knob as well as the
capacity one. Rooms up to 60 seats, with pulls batched so no single request
exceeds the service limit.
room size - so the real limit was the download side, where every client
decodes every stream it pulls. Selective subscription caps how many remote
peers any one client subscribes to, which is the cost knob as well as the
capacity one. Rooms up to 60 seats, with pulls batched so no single request
exceeds the service limit.
v8.5.0 - 8.6.7
Cam relays, hangouts stage, paid downloads
Cam relay work, the hangouts stage layout, the Arcade profile block, banner
fixes, the paid-download gate and sharded signalling.
fixes, the paid-download gate and sharded signalling.
v8.3.0 - 8.4.9
Forums, Sparks, Game Center, guides
Real message boards with nine categories, pinned and locked topics, quoting
and search. Sparks: a feed where every post burns on a clock and reactions
reset it. The Game Center moved into the dashboard, the guide auto-collects
every active mod's section, and a full security and compatibility audit came
back clean.
and search. Sparks: a feed where every post burns on a clock and reactions
reset it. The Game Center moved into the dashboard, the guide auto-collects
every active mod's section, and a full security and compatibility audit came
back clean.
v7.7.0 - 8.2.9
Editions, mod store, pricing
Buyer editions and the on-server package builder, the mod marketplace, derived
upgrade pricing so a stale number cannot survive a base-price change, and the
SEO tools.
OHS SOCIAL - RELEASE NOTES (current build: v{$clVer})
upgrade pricing so a stale number cannot survive a base-price change, and the
SEO tools.
OHS SOCIAL - RELEASE NOTES (current build: v{$clVer})
v7.6.9
OHS Spell: hover spell check
A SPELL CHECKER THAT BELONGS TO THE SITE, NOT THE BROWSER. OHS Spell marks
misspellings as members type and offers suggestions on hover, working from an
85,000-word English dictionary - the most common 46,000 of them frequency-ranked
so suggestions come back in a useful order - plus the site's own membership
vocabulary, which is what stops it flagging the words your community actually
uses.
The dictionary is served as static content: an ETag and a week-long immutable
cache mean one download per browser per build, gzipped wherever the client
accepts it, and it needs no authentication because it is a word list. Any member
who would rather not have it can switch it off for that browser, a flag the
engine reads before it boots, with no server round trip.
v7.6.8
📺 Free TV Streams
• Free TV Streams joins the platform: a built-in guide of 450+ free live US
channels (news, movies, comedy, classic TV, kids, music, food, history and
more). Members watch on their profile and together on a shared screen inside
cam rooms, where the room owner or a mod sets the channel from Room Settings
and members can suggest what to watch next. Plays in every browser through a
built-in stream proxy, with a health checker and a Refresh from source tool.
• Available in Admin > Mod Store as a $100 add-on. Where it is installed and
enabled, the in-room TV screen, profile player and channel guide turn on.
channels (news, movies, comedy, classic TV, kids, music, food, history and
more). Members watch on their profile and together on a shared screen inside
cam rooms, where the room owner or a mod sets the channel from Room Settings
and members can suggest what to watch next. Plays in every browser through a
built-in stream proxy, with a health checker and a Refresh from source tool.
• Available in Admin > Mod Store as a $100 add-on. Where it is installed and
enabled, the in-room TV screen, profile player and channel guide turn on.
v7.6.7
📝 Polls & Marketplace live under the Post menu
• FIXED - the 📊 Polls and 🏪 Marketplace links are back in the "📝 Post"
dropdown instead of sitting loose in the top nav bar. (Marketplace still
gets its own prominent link when site-wide Marketplace Mode is on.)
dropdown instead of sitting loose in the top nav bar. (Marketplace still
gets its own prominent link when site-wide Marketplace Mode is on.)
v7.6.6
🏅 Donor program: admins are permanent + multi-month manual grants
• FIXED - admin accounts are now PERMANENT donors. An admin's donor badge,
perks, storage bonus and Theme Builder access never expire, and admins are
never sent renewal reminders. (Before, an admin who had a dated donor term
could lapse and get nagged like any other member.)
• NEW - the admin "grant donor status" form now takes a length in months, so
you can comp someone donor perks for more than one month at a time (up to
50 years). Re-granting extends the current term instead of resetting it.
perks, storage bonus and Theme Builder access never expire, and admins are
never sent renewal reminders. (Before, an admin who had a dated donor term
could lapse and get nagged like any other member.)
• NEW - the admin "grant donor status" form now takes a length in months, so
you can comp someone donor perks for more than one month at a time (up to
50 years). Re-granting extends the current term instead of resetting it.
v7.6.5
📊 ROOM POLLS (PulsePoll in cam rooms) + community Paint
• Removed Trivia Night.
• NEW - Room Polls: the 📊 Polls button in cam rooms opens the full
PulsePoll surface right there. Make a poll on the spot or share any
existing poll to the room for 1 to 20 minutes. A shared poll shows in a
small strip just above the 420 clock so everyone can vote in one tap,
and it disappears when its time runs out. Only one poll runs at a time
- the rest wait in a queue and take over automatically as each one ends.
A poll you create while in a room is shared to that room automatically.
Votes ride the main PulsePoll feed, so a vote here and a vote there are
the same vote and show the same numbers, with a link to the full feed.
· This only appears on sites that have the PulsePoll mod enabled. If
the mod isn't on, the button isn't shown at all - and it's gated
again on the server, so there's no way to reach it without the mod.
(LITE is unaffected - it doesn't get any of this.)
• NEW - 🎨 OHS Paint as a standalone community feature with its own
"🎨 Paint" nav link and page, on EVERY edition including LITE. A full
paint studio (brushes, shapes, fill, spray, emoji stamps, opacity,
undo/redo, save-to-PNG) that isn't tied to any cam room - just draw
and save your art to share anywhere.
• NEW - Room Polls: the 📊 Polls button in cam rooms opens the full
PulsePoll surface right there. Make a poll on the spot or share any
existing poll to the room for 1 to 20 minutes. A shared poll shows in a
small strip just above the 420 clock so everyone can vote in one tap,
and it disappears when its time runs out. Only one poll runs at a time
- the rest wait in a queue and take over automatically as each one ends.
A poll you create while in a room is shared to that room automatically.
Votes ride the main PulsePoll feed, so a vote here and a vote there are
the same vote and show the same numbers, with a link to the full feed.
· This only appears on sites that have the PulsePoll mod enabled. If
the mod isn't on, the button isn't shown at all - and it's gated
again on the server, so there's no way to reach it without the mod.
(LITE is unaffected - it doesn't get any of this.)
• NEW - 🎨 OHS Paint as a standalone community feature with its own
"🎨 Paint" nav link and page, on EVERY edition including LITE. A full
paint studio (brushes, shapes, fill, spray, emoji stamps, opacity,
undo/redo, save-to-PNG) that isn't tied to any cam room - just draw
and save your art to share anywhere.
v7.6.4
🎨 Paint toolbar text is now readable
• FIXED - text in the paint toolbar (the Size/Opacity labels and the
toggle captions) was dark on the dark bar and unreadable. All toolbar
text is white now; buttons keep their own styling.
toggle captions) was dark on the dark bar and unreadable. All toolbar
text is white now; buttons keep their own styling.
v7.6.3
🎨 Paint fixes: readable title, labelled toggles, working Live/Solo
• FIXED - the "OHS Paint" title could be white on a light bar and
unreadable. It now sits on its own dark pill, legible on any theme.
• FIXED - the 🌐 Live / 🔒 Solo button did nothing. It (and Save, Share,
Redo) are now properly hooked up and toggle as expected: Live shares
the board with the whole room, Solo is your private scratchpad.
• The Dashed / Mirror / Grid toggles are now labelled with words, not
bare symbols, so it's clear what each one does.
unreadable. It now sits on its own dark pill, legible on any theme.
• FIXED - the 🌐 Live / 🔒 Solo button did nothing. It (and Save, Share,
Redo) are now properly hooked up and toggle as expected: Live shares
the board with the whole room, Solo is your private scratchpad.
• The Dashed / Mirror / Grid toggles are now labelled with words, not
bare symbols, so it's clear what each one does.
v7.6.2
🎨 Paint emoji stamp now uses your real emoji set
• The emoji stamp tool's quick-pick strip now pulls the full
admin-managed chat emoji set (every category, in order) instead of a
small built-in list - so Paint and chat share the exact same emojis,
and editing them in Admin -> Settings -> Chat Emojis updates Paint
automatically. The strip scrolls if the set is large.
admin-managed chat emoji set (every category, in order) instead of a
small built-in list - so Paint and chat share the exact same emojis,
and editing them in Admin -> Settings -> Chat Emojis updates Paint
automatically. The strip scrolls if the set is large.
v7.6.1
🎨 OHS PAINT II (the big one)
• FIXED - the toolbar title could be white-on-white. The toolbar now
carries its own dark chrome so nothing can vanish on any theme.
• New brushes: 🖊️ marker, 🖍️ highlighter (see-through), ✒️ calligraphy.
• New shapes with live drag previews: ➜ arrow, ■ filled rectangle,
▢ rounded rectangle, ● filled ellipse, △ triangle.
• 😀 Emoji stamp tool with a quick-pick strip, per-stroke opacity
slider, ┄ dashed outlines, 🪞 mirror mode (everything you draw is
mirrored across the middle), ▦ alignment grid (only you see it),
recent-colours row, and keyboard shortcuts (P/M/H/L/A/R/O/F/S/E/T,
Z undo, Y redo).
• 🌐 Live / 🔒 Solo toggle: Live is the classic shared board - the
whole room draws together in real time, exactly like the whiteboard
always did. Solo turns it into your private scratchpad: nothing is
sent, nothing arrives, flip back any time and the shared board
replays in.
• 📤 Share to chat: posts a snapshot of the board straight into the
room chat as an image, through the same pipeline as pasted pictures.
carries its own dark chrome so nothing can vanish on any theme.
• New brushes: 🖊️ marker, 🖍️ highlighter (see-through), ✒️ calligraphy.
• New shapes with live drag previews: ➜ arrow, ■ filled rectangle,
▢ rounded rectangle, ● filled ellipse, △ triangle.
• 😀 Emoji stamp tool with a quick-pick strip, per-stroke opacity
slider, ┄ dashed outlines, 🪞 mirror mode (everything you draw is
mirrored across the middle), ▦ alignment grid (only you see it),
recent-colours row, and keyboard shortcuts (P/M/H/L/A/R/O/F/S/E/T,
Z undo, Y redo).
• 🌐 Live / 🔒 Solo toggle: Live is the classic shared board - the
whole room draws together in real time, exactly like the whiteboard
always did. Solo turns it into your private scratchpad: nothing is
sent, nothing arrives, flip back any time and the shared board
replays in.
• 📤 Share to chat: posts a snapshot of the board straight into the
room chat as an image, through the same pipeline as pasted pictures.
v7.6.0
🎯 TRIVIA NIGHT (live quiz rounds in every cam room)
• Any room host or mod can fire up a live trivia round: pick 5-15
questions and a pace (10-30 seconds each) and everyone in the room
plays together on the same clock. Fastest correct answers score the
most (100 points + a speed bonus), answers lock the moment you tap,
and every question ends with a reveal and live standings.
• Top three finishers of a completed round win tokens automatically
(25 / 15 / 10 by default - admins can tune or disable the prizes
with the trivia_prize settings). Aborted rounds pay nothing.
• Ships with an original bank of 68 questions across ten categories -
no uploads, no external services, nothing to maintain. The server
is the only clock, so everyone sees the same question at the same
second, phones and desktops alike.
questions and a pace (10-30 seconds each) and everyone in the room
plays together on the same clock. Fastest correct answers score the
most (100 points + a speed bonus), answers lock the moment you tap,
and every question ends with a reveal and live standings.
• Top three finishers of a completed round win tokens automatically
(25 / 15 / 10 by default - admins can tune or disable the prizes
with the trivia_prize settings). Aborted rounds pay nothing.
• Ships with an original bank of 68 questions across ten categories -
no uploads, no external services, nothing to maintain. The server
is the only clock, so everyone sees the same question at the same
second, phones and desktops alike.
v7.5.9
🎨 OHS PAINT (the whiteboard grows up)
• The shared room whiteboard is now a full paint program: 🪣 bucket
fill, 🖌️ spray can, 💧 colour picker (tap any pixel to grab its
colour), ↪ Redo to go with Undo, a classic 24-colour palette, and
💾 Save - downloads the board as a PNG straight to your device, no
server storage involved.
• Still fully collaborative: fill and spray travel the same journal
as pen strokes, so everyone's board stays identical - spray dots are
seeded so remote boards get the exact same pattern, pixel for pixel.
• Toolbar wraps on phones so every tool is reachable.
fill, 🖌️ spray can, 💧 colour picker (tap any pixel to grab its
colour), ↪ Redo to go with Undo, a classic 24-colour palette, and
💾 Save - downloads the board as a PNG straight to your device, no
server storage involved.
• Still fully collaborative: fill and spray travel the same journal
as pen strokes, so everyone's board stays identical - spray dots are
seeded so remote boards get the exact same pattern, pixel for pixel.
• Toolbar wraps on phones so every tool is reachable.
v7.5.8
📱 CHAT AUTO-FITS EACH ORIENTATION + FLOATS OVER CAMS
• Rotate the phone and the chat auto-refits: portrait gets the tall
panel, landscape gets a narrower column that leaves the cams visible.
Each orientation remembers its own dragged size separately, so your
landscape shape never bleeds into portrait or vice versa.
• The chat truly floats OVER the live cams now: the dimming behind it
is gone, so you watch cams while you chat. Tapping anywhere outside
still closes it, and the top/right drag grips still resize it.
panel, landscape gets a narrower column that leaves the cams visible.
Each orientation remembers its own dragged size separately, so your
landscape shape never bleeds into portrait or vice versa.
• The chat truly floats OVER the live cams now: the dimming behind it
is gone, so you watch cams while you chat. Tapping anywhere outside
still closes it, and the top/right drag grips still resize it.
v7.5.7
📱 CHAT RESIZES + LANDSCAPE FIXED, NAG BANNER GONE
• The chat popover is drag-resizable: grab the pill on its TOP edge to
change height, the pill on its RIGHT edge to change width. Your size
is remembered on that device, and it always clamps itself to the
screen - rotate the phone and it refits automatically.
• FIXED - chat could be invisible in landscape. Opening chat focuses
the keyboard, and a landscape keyboard covers the entire bottom of
the screen - exactly where the popover sat. The popover now tracks
the keyboard and rides above it, both orientations.
• Removed the '🔄 Turn your phone sideways' banner - the stage works
both ways now, so the nag was noise.
change height, the pill on its RIGHT edge to change width. Your size
is remembered on that device, and it always clamps itself to the
screen - rotate the phone and it refits automatically.
• FIXED - chat could be invisible in landscape. Opening chat focuses
the keyboard, and a landscape keyboard covers the entire bottom of
the screen - exactly where the popover sat. The popover now tracks
the keyboard and rides above it, both orientations.
• Removed the '🔄 Turn your phone sideways' banner - the stage works
both ways now, so the nag was noise.
v7.5.6
📱 SCROLL IS BACK, PINNING STAYS
• Rooms scroll normally on phones again. The v7.5.3 scroll-lock assumed
the whole page fits one screen - on real sites with headers, banners,
and mod bars it could leave the cam area unreachable. Gone.
• The 🎥/☰/💬 buttons and the chat popover stay pinned to the device
screen WHILE you scroll. That stands on the structural fixes, not the
lock: all mobile room chrome lives directly on the page body, and the
iPhone quirk where the page body becomes its own scroll container
(dragging "fixed" elements with it) is neutralized - including a
fallback for older iPhones that don't understand the modern fix.
• The grid-growth-on-scroll bug stays dead (the tile math clamp from
v7.5.3 is what actually fixed that, and it remains).
the whole page fits one screen - on real sites with headers, banners,
and mod bars it could leave the cam area unreachable. Gone.
• The 🎥/☰/💬 buttons and the chat popover stay pinned to the device
screen WHILE you scroll. That stands on the structural fixes, not the
lock: all mobile room chrome lives directly on the page body, and the
iPhone quirk where the page body becomes its own scroll container
(dragging "fixed" elements with it) is neutralized - including a
fallback for older iPhones that don't understand the modern fix.
• The grid-growth-on-scroll bug stays dead (the tile math clamp from
v7.5.3 is what actually fixed that, and it remains).
v7.5.5
📱 FULLSCREEN STAGE (header/footer gone in rooms)
• Phone rooms are truly fullscreen now. The regular site header and
footer are hidden while you're in a room - the glass top bar already
has ⬅ back and 🚪 Rooms - so the cams get the entire screen and the
scroll-lock costs nothing: there is nothing above or below the stage
to scroll to anymore. (The lock itself is what keeps the buttons and
chat pinned to the screen - it is permanent by design, camming up
does not change it. Chat, the ☰ menu, and the users list still
scroll inside themselves.)
footer are hidden while you're in a room - the glass top bar already
has ⬅ back and 🚪 Rooms - so the cams get the entire screen and the
scroll-lock costs nothing: there is nothing above or below the stage
to scroll to anymore. (The lock itself is what keeps the buttons and
chat pinned to the screen - it is permanent by design, camming up
does not change it. Chat, the ☰ menu, and the users list still
scroll inside themselves.)
v7.5.4
📱 CAM UP OUT FRONT + MENU IS A TRUE OVERLAY
• 🎥 Cam Up is back out front on phones: a big labeled pill in the
bottom-right cluster, left of ☰ and 💬 - one tap, no menu digging.
(It flips to Cam Down while you're live, as always.) Leave stays in
the ☰ menu.
• FIXED - the ☰ menu could render glued below the cams instead of
sliding up as an overlay. All the mobile room chrome (top bar, corner
buttons, menu sheet, backdrops) was rendered inside the room layout
markup, where a themed ancestor can silently break "fixed"
positioning. Every piece of that chrome now mounts directly on the
page body while mobile rooms are active - same cure the chat panel
got - so menus and chat always open as true overlays on top of the
stage. Desktop untouched.
bottom-right cluster, left of ☰ and 💬 - one tap, no menu digging.
(It flips to Cam Down while you're live, as always.) Leave stays in
the ☰ menu.
• FIXED - the ☰ menu could render glued below the cams instead of
sliding up as an overlay. All the mobile room chrome (top bar, corner
buttons, menu sheet, backdrops) was rendered inside the room layout
markup, where a themed ancestor can silently break "fixed"
positioning. Every piece of that chrome now mounts directly on the
page body while mobile rooms are active - same cure the chat panel
got - so menus and chat always open as true overlays on top of the
stage. Desktop untouched.
v7.5.3
📱 ROOM PAGE SCROLL-LOCKED + GRID GROWTH FIXED
• FIXED - the cam area could GROW as you scrolled. The tile sizer used
"viewport minus the grid's top edge" for available height - once you
scrolled, that top edge went negative and the math handed the grid
extra height equal to how far you'd scrolled. iPhones fire viewport
events mid-scroll, so every flick re-ran the math: bigger grid, more
scroll room, bigger grid. Clamped.
• The room page itself no longer scrolls on phones AT ALL. The stage
already fits everything to the screen; page scrolling only existed as
a bug (above) and was also what dragged the ☰/💬 buttons and chat off
the screen bottom on iPhones - a page that can't scroll can't scroll
anything away, on any iOS version, under any theme. Menus, chat, and
the users list still scroll inside themselves like always.
"viewport minus the grid's top edge" for available height - once you
scrolled, that top edge went negative and the math handed the grid
extra height equal to how far you'd scrolled. iPhones fire viewport
events mid-scroll, so every flick re-ran the math: bigger grid, more
scroll room, bigger grid. Clamped.
• The room page itself no longer scrolls on phones AT ALL. The stage
already fits everything to the screen; page scrolling only existed as
a bug (above) and was also what dragged the ☰/💬 buttons and chat off
the screen bottom on iPhones - a page that can't scroll can't scroll
anything away, on any iOS version, under any theme. Menus, chat, and
the users list still scroll inside themselves like always.
v7.5.2
📱 PINNED MEANS PINNED (iOS SCROLL FIX)
• FIXED - the chat popover and the ☰/💬 corner buttons could start out
in the right spot and then slide away as you scrolled, ending up stuck
at the bottom of the cam area. Root cause: the mobile "nothing widens
the page" rule used overflow-x:hidden on the page body - on iPhones
that quietly turns the body itself into the scroll container, and
every pinned element scrolls with it. Same protection now uses
overflow-x:clip, which clips without creating a scroller, so pinned
chrome pins to the DEVICE SCREEN and stays there.
• Belt on top: on phone rooms the app force-clears any transform,
filter, or overflow trick a custom theme might put on the page body -
those silently re-anchor pinned elements too. Desktop untouched.
• FIXED - the chat panel's ✕ close button was showing on DESKTOP, where
the chat is a permanent panel with nothing to close. It was hidden
only inside the phone stylesheet; it's hidden globally now and shown
only on phones.
in the right spot and then slide away as you scrolled, ending up stuck
at the bottom of the cam area. Root cause: the mobile "nothing widens
the page" rule used overflow-x:hidden on the page body - on iPhones
that quietly turns the body itself into the scroll container, and
every pinned element scrolls with it. Same protection now uses
overflow-x:clip, which clips without creating a scroller, so pinned
chrome pins to the DEVICE SCREEN and stays there.
• Belt on top: on phone rooms the app force-clears any transform,
filter, or overflow trick a custom theme might put on the page body -
those silently re-anchor pinned elements too. Desktop untouched.
• FIXED - the chat panel's ✕ close button was showing on DESKTOP, where
the chat is a permanent panel with nothing to close. It was hidden
only inside the phone stylesheet; it's hidden globally now and shown
only on phones.
v7.5.1
📱 NO EMPTY SEATS + CHAT PINNED TO THE SCREEN
• Mobile rooms show live cams ONLY, popping in one by one as people cam
up - hangouts style. No more wall of empty seats: with nobody on cam
the stage is a slim strip that grows the moment the first cam arrives.
Cam Up still lives in the ☰ menu.
• The chat popover is pinned to the bottom-left of the actual SCREEN and
stays put while you scroll - it can never end up stuck to the bottom
of the cam area again. (Under the hood the chat panel now lives
directly on the page body on phones, so no theme or layout effect can
re-anchor it.) The ☰ and 💬 buttons keep their fixed corner while
scrolling too.
up - hangouts style. No more wall of empty seats: with nobody on cam
the stage is a slim strip that grows the moment the first cam arrives.
Cam Up still lives in the ☰ menu.
• The chat popover is pinned to the bottom-left of the actual SCREEN and
stays put while you scroll - it can never end up stuck to the bottom
of the cam area again. (Under the hood the chat panel now lives
directly on the page body on phones, so no theme or layout effect can
re-anchor it.) The ☰ and 💬 buttons keep their fixed corner while
scrolling too.
v7.5.0
📱 MOBILE ROOMS: CHAT IS A POPOVER + CORNER BUTTONS
• Mobile cam rooms now keep exactly two floating buttons in the bottom
right corner: ☰ opens the room menu, 💬 opens the chat. Cam Up and
Leave moved to a big action row at the top of the ☰ menu.
• Chat on phones is a floating popover anchored above the 💬 button -
it NEVER sits in the page below the cams anymore, in any room layout.
Closed means gone; open means a clean rounded panel you can dismiss by
tapping anywhere outside it.
• The 💬 button shows a live count bubble: it starts with the number of
messages already in the room and ticks up as new ones arrive while the
chat is closed. Opening the chat clears it.
• FIXED - the mobile Chat/Menu buttons and the mobile menu contents were
leaking onto DESKTOP cam rooms as plain unstyled buttons. The rules
that hid them only existed inside the phone stylesheet, which desktop
never reads. They're now hidden everywhere by default and enabled only
on phones.
• LITE - removed the Daily Token Lottery everywhere (nav link, page,
achievements, welcome note). LITE has no token economy, so a lottery
made no sense there. Paid editions are unchanged.
right corner: ☰ opens the room menu, 💬 opens the chat. Cam Up and
Leave moved to a big action row at the top of the ☰ menu.
• Chat on phones is a floating popover anchored above the 💬 button -
it NEVER sits in the page below the cams anymore, in any room layout.
Closed means gone; open means a clean rounded panel you can dismiss by
tapping anywhere outside it.
• The 💬 button shows a live count bubble: it starts with the number of
messages already in the room and ticks up as new ones arrive while the
chat is closed. Opening the chat clears it.
• FIXED - the mobile Chat/Menu buttons and the mobile menu contents were
leaking onto DESKTOP cam rooms as plain unstyled buttons. The rules
that hid them only existed inside the phone stylesheet, which desktop
never reads. They're now hidden everywhere by default and enabled only
on phones.
• LITE - removed the Daily Token Lottery everywhere (nav link, page,
achievements, welcome note). LITE has no token economy, so a lottery
made no sense there. Paid editions are unchanged.
v7.4.5
📱 CHAT SHEET UN-BRICKED ON PHONES
• FIXED - the 💬 chat sheet could refuse to open on phones no matter how
many times you tapped the button. Cause: the old minimize-to-bubble
chat from years back saved a "minimized" flag on your phone per room.
That flag still got restored on load, and its old styling pinned the
chat off-screen and unclickable - while the little bubble that used to
undo it no longer exists in the new room design. One stale flag from
the old days = chat permanently dead in that room on that phone. The
new sheet now always outranks the old minimize styling, and cam room
pages clean the stale flag off your phone automatically.
many times you tapped the button. Cause: the old minimize-to-bubble
chat from years back saved a "minimized" flag on your phone per room.
That flag still got restored on load, and its old styling pinned the
chat off-screen and unclickable - while the little bubble that used to
undo it no longer exists in the new room design. One stale flag from
the old days = chat permanently dead in that room on that phone. The
new sheet now always outranks the old minimize styling, and cam room
pages clean the stale flag off your phone automatically.
v7.4.4
📱 THE iPHONE FREEZE, FOUND AND KILLED
• FIXED - the real cam-up freeze on phones. The Hangouts layout engine
watches cam tiles for changes and was re-marking tiles with a CSS class
they already had, every time it was poked. Desktop Chrome ignores those
do-nothing writes, but iPhones (and other strict browsers) report each
one as a fresh change - so the watcher poked itself in an infinite loop
the instant your own cam tile appeared. The page hard-froze at exactly
the moment you cammed up, only on phones, in rooms using the Hangouts
layout. Every class write in that engine is now change-guarded, its
watcher settles once per frame instead of instantly re-firing, and the
same discipline was applied to the speaking-ring watcher. Reproduced in
a simulator before the fix, clean after.
• Hangouts (floating draggable cams) no longer tries to engage on phones
at all - the phone stage layout always wins. Dragging floating boxes on
a touch screen never made sense anyway; picking Hangouts on desktop no
longer risks anything on your phone.
• The glass top bar and dock on phones dropped their background blur.
Blurring live video behind a bar is one of the heaviest effects a phone
GPU can do; a slightly deeper tint looks the same and costs nothing.
watches cam tiles for changes and was re-marking tiles with a CSS class
they already had, every time it was poked. Desktop Chrome ignores those
do-nothing writes, but iPhones (and other strict browsers) report each
one as a fresh change - so the watcher poked itself in an infinite loop
the instant your own cam tile appeared. The page hard-froze at exactly
the moment you cammed up, only on phones, in rooms using the Hangouts
layout. Every class write in that engine is now change-guarded, its
watcher settles once per frame instead of instantly re-firing, and the
same discipline was applied to the speaking-ring watcher. Reproduced in
a simulator before the fix, clean after.
• Hangouts (floating draggable cams) no longer tries to engage on phones
at all - the phone stage layout always wins. Dragging floating boxes on
a touch screen never made sense anyway; picking Hangouts on desktop no
longer risks anything on your phone.
• The glass top bar and dock on phones dropped their background blur.
Blurring live video behind a bar is one of the heaviest effects a phone
GPU can do; a slightly deeper tint looks the same and costs nothing.
v7.4.3
📱 CAM UP FREEZE FIX + FASTER MOBILE
• FIXED - tapping Cam Up on a phone could freeze. The camera picker was
starting the camera once just to read device names, stopping it, then
starting it AGAIN after you picked - and phone camera hardware caught
mid-release can leave that second start hanging forever. Phones now get
a simple Front / Rear choice that needs no warm-up: one tap, ONE camera
start, done. The requested capture mode also dropped its hard caps,
which some phone cameras refuse outright.
• Every camera start now has a watchdog: if the hardware wedges anyway
(another app holding the camera, a slow release), you get a clear
"taking too long - close other camera apps and try again" message in
seconds instead of a frozen page, and the stale start is cleaned up so
the next tap works.
• Pages are now sent compressed. The cam room page shrinks about 75% on
the wire, which is most of the wait on a phone connection - every page
on the site loads noticeably faster, on any device.
• Images across the site lazy-load: photos and avatars below the fold no
longer compete with the page you're actually looking at.
starting the camera once just to read device names, stopping it, then
starting it AGAIN after you picked - and phone camera hardware caught
mid-release can leave that second start hanging forever. Phones now get
a simple Front / Rear choice that needs no warm-up: one tap, ONE camera
start, done. The requested capture mode also dropped its hard caps,
which some phone cameras refuse outright.
• Every camera start now has a watchdog: if the hardware wedges anyway
(another app holding the camera, a slow release), you get a clear
"taking too long - close other camera apps and try again" message in
seconds instead of a frozen page, and the stale start is cleaned up so
the next tap works.
• Pages are now sent compressed. The cam room page shrinks about 75% on
the wire, which is most of the wait on a phone connection - every page
on the site loads noticeably faster, on any device.
• Images across the site lazy-load: photos and avatars below the fold no
longer compete with the page you're actually looking at.
v7.4.2
📱 MOBILE ROOMS REPAIR + ANIMATIONS OFF ON PHONES
• FIXED - on phones, the retired pre-v5 mobile room styles were still
firing underneath the new stage design and winning some fights: the
chat panel was forced permanently open as a 45-47% dock covering the
bottom cam rows, and the action dock was hoisted to the middle of the
screen. Tapping a seat (or Cam Up) looked broken because the seat and
your fresh tile could sit underneath the docked chat. The old styles
are fully removed; the stage is the only phone layout now, exactly as
v7.4.1 intended: cams edge to edge, chat closed until you open it,
the dock floating at the bottom.
• FIXED - the camera source picker offered "Share a screen or window"
on phones that can't screen share (iPhone Safari and most mobile
browsers). Tapping it silently cancelled the whole cam-up. The option
now only appears where the browser supports it.
• Animated backgrounds are now off on phones automatically - they burn
battery and steal power from live cams even when they look smooth. A
one-tap note explains it, with a "Turn them back on" button that
re-enables them for that device only and never nags again. Desktop is
untouched, and a member's account-wide animation setting is never
changed by their phone.
firing underneath the new stage design and winning some fights: the
chat panel was forced permanently open as a 45-47% dock covering the
bottom cam rows, and the action dock was hoisted to the middle of the
screen. Tapping a seat (or Cam Up) looked broken because the seat and
your fresh tile could sit underneath the docked chat. The old styles
are fully removed; the stage is the only phone layout now, exactly as
v7.4.1 intended: cams edge to edge, chat closed until you open it,
the dock floating at the bottom.
• FIXED - the camera source picker offered "Share a screen or window"
on phones that can't screen share (iPhone Safari and most mobile
browsers). Tapping it silently cancelled the whole cam-up. The option
now only appears where the browser supports it.
• Animated backgrounds are now off on phones automatically - they burn
battery and steal power from live cams even when they look smooth. A
one-tap note explains it, with a "Turn them back on" button that
re-enables them for that device only and never nags again. Desktop is
untouched, and a member's account-wide animation setting is never
changed by their phone.
v7.4.1
📱 MOBILE ROOMS v5 "THE STAGE" + 💸 BUTTON PLACEMENT
• Mobile rooms rebuilt as a stage: near-black backdrop, cams edge to
edge, a slim glass top bar (back to site · room name · rooms list),
and a floating glass dock with Cam Up · 💬 Chat · ☰ Menu · Leave.
• Chat and the menu are bottom sheets with grabber handles that slide
over the cams. Chat opens with the composer ready and an unread
counter ticks on the dock while it's closed.
• The design is now server-rendered and CSS-driven - it appears the
instant the page paints, whether or not scripts have loaded. v4
depended on a script running first; if that script stumbled, phones
silently kept the old layout. That whole failure mode is gone.
• 💸 Send Tokens is a proper button in the friend action row on
profiles, right beside Send Message - tap it to unfold the amount
and note. (It was there before, but tucked above the row as a
fold-out that was too easy to miss.)
edge, a slim glass top bar (back to site · room name · rooms list),
and a floating glass dock with Cam Up · 💬 Chat · ☰ Menu · Leave.
• Chat and the menu are bottom sheets with grabber handles that slide
over the cams. Chat opens with the composer ready and an unread
counter ticks on the dock while it's closed.
• The design is now server-rendered and CSS-driven - it appears the
instant the page paints, whether or not scripts have loaded. v4
depended on a script running first; if that script stumbled, phones
silently kept the old layout. That whole failure mode is gone.
• 💸 Send Tokens is a proper button in the friend action row on
profiles, right beside Send Message - tap it to unfold the amount
and note. (It was there before, but tucked above the row as a
fold-out that was too easy to miss.)
v7.4.0
📱 MOBILE ROOMS v4: CAMS FIRST
• On phones, cam rooms are now cams edge-to-edge. The full site menu is
replaced by a slim bar: back to the site on the left, the room name in
the middle, the rooms list on the right.
• Text chat is a slide-up overlay instead of a permanent dock: closed by
default, opened from the 💬 Chat button (with an unread counter), sits
over the cams with the composer ready, and closes from ✕ or by tapping
outside. The old always-docked strip covered the bottom 45% of the
screen AND overlapped the bottom cam row - both gone.
• The ⋯ Options sheet is now ☰ Menu and gained the live In-Room users
list plus one-tap Home and Rooms links; every cam tool still lives
there. Cam Up, 💬 Chat, ☰ Menu and Leave sit on one thumb-height bar.
replaced by a slim bar: back to the site on the left, the room name in
the middle, the rooms list on the right.
• Text chat is a slide-up overlay instead of a permanent dock: closed by
default, opened from the 💬 Chat button (with an unread counter), sits
over the cams with the composer ready, and closes from ✕ or by tapping
outside. The old always-docked strip covered the bottom 45% of the
screen AND overlapped the bottom cam row - both gone.
• The ⋯ Options sheet is now ☰ Menu and gained the live In-Room users
list plus one-tap Home and Rooms links; every cam tool still lives
there. Cam Up, 💬 Chat, ☰ Menu and Leave sit on one thumb-height bar.
v7.3.0
🌟 TOP FRIENDS, 💸 SEND TOKENS, ⭐ WISHLISTS + OPS PACK
• Top Friends grids now come in MySpace-style sizes: 4, 8, 12, 16, 20, 24,
32 or 40 friends. Members pick a size and drag-order friends in Edit
Profile; big shelves auto-pack into a tighter 4-across grid.
• Members can send tokens to each other (friends-only by default) from a
friend's profile or the Buy Tokens page. Admin panel has the switches:
on/off, friends-only, optional house fee %, minimum send, daily cap.
• Store wishlists: members star items they want; a "⭐ Wants" strip shows
on their profile Collection card and item pages show which of your
friends want them - instant gift ideas for the 🎁 gifting system.
• Client Portal now has an Orders tab with printable receipts for every
purchase (script tiers and mod-store orders both included).
• Devices tab in Settings: see every browser signed into your account and
log out any of them remotely (or everything except this one).
• Online Now can show recently-idle members with a 💤 away badge
(admin-tunable idle window, on by default).
• Announcements can now ring members' bells too: tick 🔔 when posting
and every member (or admins only, for a test run) gets a notification,
so people who aren't around while the banner is up still hear about it.
• Admin Ops tab: a JSON health scanner with one-click reseed for any
missing data files.
• Slow devices now auto-switch animated backgrounds off and tell the
member with a dismissible note (one tap turns them back on and stops
future auto-switching). Manual toggles always win.
32 or 40 friends. Members pick a size and drag-order friends in Edit
Profile; big shelves auto-pack into a tighter 4-across grid.
• Members can send tokens to each other (friends-only by default) from a
friend's profile or the Buy Tokens page. Admin panel has the switches:
on/off, friends-only, optional house fee %, minimum send, daily cap.
• Store wishlists: members star items they want; a "⭐ Wants" strip shows
on their profile Collection card and item pages show which of your
friends want them - instant gift ideas for the 🎁 gifting system.
• Client Portal now has an Orders tab with printable receipts for every
purchase (script tiers and mod-store orders both included).
• Devices tab in Settings: see every browser signed into your account and
log out any of them remotely (or everything except this one).
• Online Now can show recently-idle members with a 💤 away badge
(admin-tunable idle window, on by default).
• Announcements can now ring members' bells too: tick 🔔 when posting
and every member (or admins only, for a test run) gets a notification,
so people who aren't around while the banner is up still hear about it.
• Admin Ops tab: a JSON health scanner with one-click reseed for any
missing data files.
• Slow devices now auto-switch animated backgrounds off and tell the
member with a dismissible note (one tap turns them back on and stops
future auto-switching). Manual toggles always win.
v7.2.1
📱 Rooms: the composer is reachable on EVERY phone now
* FIXED - on phones browsing with a desktop-size viewport (Safari/Chrome
"Request Desktop Site", Samsung Internet's desktop default, some
tablets), rooms fell back to the full desktop multi-pane layout crushed
onto the screen: the chat box sat a whole page-scroll below the cams
and user list. The mobile room system (pinned bottom chat strip,
always-visible type box, slim action bar) is now keyed to TOUCH, not
just screen width - it engages on any touch device up to 1180px wide,
portrait and landscape, no matter what viewport the browser reports.
* Desktop stays desktop: mouse/trackpad browsers at any width are
untouched, and nothing about the tuned cam tiling changed.
"Request Desktop Site", Samsung Internet's desktop default, some
tablets), rooms fell back to the full desktop multi-pane layout crushed
onto the screen: the chat box sat a whole page-scroll below the cams
and user list. The mobile room system (pinned bottom chat strip,
always-visible type box, slim action bar) is now keyed to TOUCH, not
just screen width - it engages on any touch device up to 1180px wide,
portrait and landscape, no matter what viewport the browser reports.
* Desktop stays desktop: mouse/trackpad browsers at any width are
untouched, and nothing about the tuned cam tiling changed.
v7.2.0
📱 Mobile hardening: the whole site behaves on phones
* No toggle, no separate mode - the layout adapts to portrait AND
landscape. The responsive base (hamburger nav, stacking grids, the
landscape header fix) was already live; this release closes the gaps
that still made phones feel broken:
* Form fields are 16px on small screens, so iOS Safari stops zoom-jumping
the page every time a field is tapped.
* Tables (admin queues, backups, licenses, ticket lists, anywhere) now
scroll inside their own card instead of dragging the whole page
sideways - the #1 cause of the pan-and-zoom mess.
* Nothing can widen the page anymore: long tokens, pasted links and code
wrap; images, video and embeds cap at screen width.
* Nav dropdown menus open inline at full width inside the mobile menu
instead of clipping off the screen edge.
* Buttons and tabs get proper touch-sized targets, and landscape phones
reclaim vertical space (tighter cards, smaller headings) so content -
not chrome - owns the short screen.
landscape. The responsive base (hamburger nav, stacking grids, the
landscape header fix) was already live; this release closes the gaps
that still made phones feel broken:
* Form fields are 16px on small screens, so iOS Safari stops zoom-jumping
the page every time a field is tapped.
* Tables (admin queues, backups, licenses, ticket lists, anywhere) now
scroll inside their own card instead of dragging the whole page
sideways - the #1 cause of the pan-and-zoom mess.
* Nothing can widen the page anymore: long tokens, pasted links and code
wrap; images, video and embeds cap at screen width.
* Nav dropdown menus open inline at full width inside the mobile menu
instead of clipping off the screen edge.
* Buttons and tabs get proper touch-sized targets, and landscape phones
reclaim vertical space (tighter cards, smaller headings) so content -
not chrome - owns the short screen.
v7.1.3
Buy-page copy fix
* Profile Music was listed under "Free included games" on the BUY IT ALL
card. It's not a game - it's the free MySpace-style profile player base
mod - so it now has its own line where it belongs.
card. It's not a game - it's the free MySpace-style profile player base
mod - so it now has its own line where it belongs.
v7.1.2
The real Support system, embedded in the Client Portal (helpdesk v1.1.0)
* The portal's Support tab now IS the site's ?page=support system - the
same knowledge base, ticket list, threads and reply forms, rendered
right inside the portal and keyed to the client's purchase email.
* The Helpdesk itself is now client-aware everywhere: a logged-in portal
client visiting ?page=support directly ALSO sees their tickets, can
open threads, and can reply - identity comes from their portal session,
never a form field. Ticket forms show "Filing as <their email>" with
guest fields hidden. Every link and redirect returns to wherever the
form lived (the portal tab or the support page).
* Permissions hold on both sides: clients see and touch only tickets
matching their own email; members keep their user-keyed access;
strangers see neither.
same knowledge base, ticket list, threads and reply forms, rendered
right inside the portal and keyed to the client's purchase email.
* The Helpdesk itself is now client-aware everywhere: a logged-in portal
client visiting ?page=support directly ALSO sees their tickets, can
open threads, and can reply - identity comes from their portal session,
never a form field. Ticket forms show "Filing as <their email>" with
guest fields hidden. Every link and redirect returns to wherever the
form lived (the portal tab or the support page).
* Permissions hold on both sides: clients see and touch only tickets
matching their own email; members keep their user-keyed access;
strangers see neither.
v7.1.1
One ticket queue + docs that always show the right version
* Client Portal tickets now live in the HELPDESK: portal tickets land in
the same queue as member tickets (category "Client Portal", the buyer's
email on the row), you answer them in the normal Helpdesk admin tab,
and the helpdesk's staff-reply email reaches the client automatically.
Any tickets from the old portal-only store migrate over on their own.
* FIXED - the portal's Install Guide link served the raw template, so
clients saw a literal "OHS Social v{{OHS_VERSION}}". Both guides now
render through the portal with the live version stamped - always
current, no rebuild needed.
* FIXED - the Mod Builder Guide header claimed v3.5.x (it had said that
for four major versions). The guide's version is now stamped at build
time from the real release number, permanently.
the same queue as member tickets (category "Client Portal", the buyer's
email on the row), you answer them in the normal Helpdesk admin tab,
and the helpdesk's staff-reply email reaches the client automatically.
Any tickets from the old portal-only store migrate over on their own.
* FIXED - the portal's Install Guide link served the raw template, so
clients saw a literal "OHS Social v{{OHS_VERSION}}". Both guides now
render through the portal with the live version stamped - always
current, no rebuild needed.
* FIXED - the Mod Builder Guide header claimed v3.5.x (it had said that
for four major versions). The guide's version is now stamped at build
time from the real release number, permanently.
v7.1.0
🧑💼 CLIENT PORTAL (master/seller feature)
* Sellers now have a full customer area at ?page=client. Buyers land there
straight from their fulfillment email, claim their account with the
purchase email + their download token, set a password once, and get:
⬇️ Downloads - the current build of their edition (white-label buyers
also get the no-brand LITE companion) plus a fresh zip of every
premium mod they own, rebuilt automatically for each new version.
🎫 Support - private ticket threads straight to you, with email pings
on new tickets and reply/close from your License Manager.
📚 Docs - mod guide, install guide, and release-notes pointers.
👤 Account - password changes plus live license status for each of
their domains.
* Ownership is DERIVED from sales.json + mod_sales.json by purchase email
- upgrades rank toward their target tier, refunds don't count, pending
mod orders don't count. Zero double bookkeeping.
* You can also mint or extend accounts by hand: License Manager -> Client
Portal card (email, optional password, tier override, mod grants, extra
domains). Client tickets flow into the Helpdesk mod - one queue for everything.
* Fulfillment emails now include the portal link and claim instructions;
the buy page links existing customers straight in.
straight from their fulfillment email, claim their account with the
purchase email + their download token, set a password once, and get:
⬇️ Downloads - the current build of their edition (white-label buyers
also get the no-brand LITE companion) plus a fresh zip of every
premium mod they own, rebuilt automatically for each new version.
🎫 Support - private ticket threads straight to you, with email pings
on new tickets and reply/close from your License Manager.
📚 Docs - mod guide, install guide, and release-notes pointers.
👤 Account - password changes plus live license status for each of
their domains.
* Ownership is DERIVED from sales.json + mod_sales.json by purchase email
- upgrades rank toward their target tier, refunds don't count, pending
mod orders don't count. Zero double bookkeeping.
* You can also mint or extend accounts by hand: License Manager -> Client
Portal card (email, optional password, tier override, mod grants, extra
domains). Client tickets flow into the Helpdesk mod - one queue for everything.
* Fulfillment emails now include the portal link and claim instructions;
the buy page links existing customers straight in.
v7.0.3
💾 Scheduled auto-backups + 🎁 new-member welcome grant
* NEW - automatic backups on YOUR schedule: Admin -> 💾 Backups now has a
"run every N days" picker (Off / 1 / 2 / 3 / 4 / 5 / 7 / 14 / 30) and a
keep-newest-N retention setting. The snapshot runs at the START of daily
maintenance - before anything touches the data. Defaults: daily, keep 14.
* Backups are finally COMPLETE: every backup now includes the wallet
ledger (.jsonl) and the store item tree alongside the root .json files -
the old backup silently skipped both. Hosts without php-zip get a plain
folder snapshot instead of no backup at all.
* Each backup in the list now has ⬇️ Download and 🗑️ Delete buttons, and
the tab shows when the last backup ran and when the next one is due.
* NEW - 🎁 New member welcome grant: every fresh signup receives a token
gift (default 25, set it in the ⚙️ Community Perks card on the Lottery
page - 0 turns it off) with a welcome notification pointing them at the
store and the daily lottery. Locked, ledgered, works on every edition,
and approval-gated signups get theirs too - it waits in their wallet.
"run every N days" picker (Off / 1 / 2 / 3 / 4 / 5 / 7 / 14 / 30) and a
keep-newest-N retention setting. The snapshot runs at the START of daily
maintenance - before anything touches the data. Defaults: daily, keep 14.
* Backups are finally COMPLETE: every backup now includes the wallet
ledger (.jsonl) and the store item tree alongside the root .json files -
the old backup silently skipped both. Hosts without php-zip get a plain
folder snapshot instead of no backup at all.
* Each backup in the list now has ⬇️ Download and 🗑️ Delete buttons, and
the tab shows when the last backup ran and when the next one is due.
* NEW - 🎁 New member welcome grant: every fresh signup receives a token
gift (default 25, set it in the ⚙️ Community Perks card on the Lottery
page - 0 turns it off) with a welcome notification pointing them at the
store and the daily lottery. Locked, ledgered, works on every edition,
and approval-gated signups get theirs too - it waits in their wallet.
v7.0.2
Hotfix: streak faucet fired on every page load
* FIXED - the daily login streak reward could pay out on EVERY page view
instead of once per day. The credit step reloaded the member list from
disk mid-update, which erased the "already rewarded today" stamp before
it was saved - so the site never remembered paying. Tokens and the
daily stamp now travel in the same write; the reward is once per day
again, still recorded in the wallet ledger.
* If your site ran v7.0.0/v7.0.1 for a while: members gained roughly
(base reward × pages they viewed that day) extra tokens. Streak counts
and tiers were NOT inflated. Harmless for most communities; adjust any
outliers from the admin wallet tools if you care to.
instead of once per day. The credit step reloaded the member list from
disk mid-update, which erased the "already rewarded today" stamp before
it was saved - so the site never remembered paying. Tokens and the
daily stamp now travel in the same write; the reward is once per day
again, still recorded in the wallet ledger.
* If your site ran v7.0.0/v7.0.1 for a while: members gained roughly
(base reward × pages they viewed that day) extra tokens. Streak counts
and tiers were NOT inflated. Harmless for most communities; adjust any
outliers from the admin wallet tools if you care to.
v7.0.1
CRITICAL data-safety hotfix + license fix. INSTALL BEFORE v7.0.0.
* FIXED (critical) - the v7.0.0 achievements launch backfill rewrote
users.json once per member per badge in a single request. On a large,
established site PHP could kill that request mid-write and leave
users.json empty - which the first-run bootstrap then treated as a NEW
INSTALL. If you already deployed v7.0.0 and this hit you: restore
ohs_data/users.json from your host's most recent backup, keep the
ohs_data/.daily_maintenance_* marker in place, install this build, THEN
the nightly sweep is safe to run.
* The backfill is now ONE locked pass over users.json (milliseconds on
hundreds of members), and the first run is SILENT - badges recorded, no
notification storm, no token airdrop. Nightly runs after that reward
only genuinely new unlocks, capped.
* Locked file writes are now crash-safe everywhere: full payload to a
temp file, atomic rename into place. A killed request can no longer
leave a half-written data file - it's always the complete old version
or the complete new one.
* users.json / profiles.json / settings.json that are damaged on disk are
now QUARANTINED (a .corrupt.* copy is preserved) and the site HALTS
with instructions instead of silently running as empty - and the
first-run admin bootstrap refuses to re-seed while a quarantined file
is awaiting restore. Loud beats data loss, permanently.
* FIXED - domain licenses could bind to a cPanel service alias: AutoSSL
or any bot hitting mail.yourdomain.com (same docroot as the apex) could
be a fresh site's very first request, binding the license to the alias
- then your first real visit read as a "domain move" and started the
grace countdown. All license comparisons now canonicalise away service
prefixes (www, mail, webmail, cpanel, whm, webdisk, ftp, autodiscover,
autoconfig), mis-bound records self-heal on both the site and the
license server, and real subdomain installs are untouched.
users.json once per member per badge in a single request. On a large,
established site PHP could kill that request mid-write and leave
users.json empty - which the first-run bootstrap then treated as a NEW
INSTALL. If you already deployed v7.0.0 and this hit you: restore
ohs_data/users.json from your host's most recent backup, keep the
ohs_data/.daily_maintenance_* marker in place, install this build, THEN
the nightly sweep is safe to run.
* The backfill is now ONE locked pass over users.json (milliseconds on
hundreds of members), and the first run is SILENT - badges recorded, no
notification storm, no token airdrop. Nightly runs after that reward
only genuinely new unlocks, capped.
* Locked file writes are now crash-safe everywhere: full payload to a
temp file, atomic rename into place. A killed request can no longer
leave a half-written data file - it's always the complete old version
or the complete new one.
* users.json / profiles.json / settings.json that are damaged on disk are
now QUARANTINED (a .corrupt.* copy is preserved) and the site HALTS
with instructions instead of silently running as empty - and the
first-run admin bootstrap refuses to re-seed while a quarantined file
is awaiting restore. Loud beats data loss, permanently.
* FIXED - domain licenses could bind to a cPanel service alias: AutoSSL
or any bot hitting mail.yourdomain.com (same docroot as the apex) could
be a fresh site's very first request, binding the license to the alias
- then your first real visit read as a "domain move" and started the
grace countdown. All license comparisons now canonicalise away service
prefixes (www, mail, webmail, cpanel, whm, webdisk, ftp, autodiscover,
autoconfig), mis-bound records self-heal on both the site and the
license server, and real subdomain installs are untouched.
v7.0.0
🎆 THE COMMUNITY PERKS RELEASE: Lottery · Achievements · Visitors · Birthdays
Four brand-new member-facing features, all core, all editions (LITE too),
all riding the locked + ledgered token engine introduced in v6.9.9 - which
means every one of them works flawlessly inside the nightly maintenance
cycle. One ⚙️ admin card (on the Lottery page, admins only) controls it all.
🎟️ DAILY TOKEN LOTTERY
* Members buy tickets (price, per-member daily cap, house cut, and daily
pot seed are all yours to set). One winner drawn every night; the prize
lands in their wallet with a notification and a ledger entry. Nobody
played? The pot ROLLS OVER and grows - come back tomorrow.
* Double-submit safe: the per-day cap is re-checked inside the file lock
and anything that doesn't fit refunds itself automatically.
* Community -> 🎟️ Lottery. Live pot, ticket drum count, recent winners.
🏅 ACHIEVEMENTS
* Twelve unlockable badges: first post, storyteller, friends milestones,
login-streak milestones, store collecting, birthday club, lottery play,
and the jackpot. Each unlock fires a notification and an optional token
reward (default 25 - set it to 0 before deploying if you'd rather skip
the one-time launch backfill every member receives for badges they
already earned).
* Badges show on every profile as a draggable sidebar card; the full
gallery lives at Community -> 🏅 Achievements with locked ones grayed.
👀 PROFILE VISITORS
* The classic "who viewed me": members see their last 8 profile visitors
with relative timestamps - and ONLY the owner ever sees the card.
* Privacy first: a "👻 Browse profiles invisibly" switch in Settings keeps
a member out of everyone's visitor lists entirely.
🎂 BIRTHDAYS
* Members set month + day in Settings (validated; year never asked).
On the day: a token gift (amount yours to set), a site notification,
and a spot on the homepage "Today's Birthdays" card. Gifted exactly
once per year, race-proof.
all riding the locked + ledgered token engine introduced in v6.9.9 - which
means every one of them works flawlessly inside the nightly maintenance
cycle. One ⚙️ admin card (on the Lottery page, admins only) controls it all.
🎟️ DAILY TOKEN LOTTERY
* Members buy tickets (price, per-member daily cap, house cut, and daily
pot seed are all yours to set). One winner drawn every night; the prize
lands in their wallet with a notification and a ledger entry. Nobody
played? The pot ROLLS OVER and grows - come back tomorrow.
* Double-submit safe: the per-day cap is re-checked inside the file lock
and anything that doesn't fit refunds itself automatically.
* Community -> 🎟️ Lottery. Live pot, ticket drum count, recent winners.
🏅 ACHIEVEMENTS
* Twelve unlockable badges: first post, storyteller, friends milestones,
login-streak milestones, store collecting, birthday club, lottery play,
and the jackpot. Each unlock fires a notification and an optional token
reward (default 25 - set it to 0 before deploying if you'd rather skip
the one-time launch backfill every member receives for badges they
already earned).
* Badges show on every profile as a draggable sidebar card; the full
gallery lives at Community -> 🏅 Achievements with locked ones grayed.
👀 PROFILE VISITORS
* The classic "who viewed me": members see their last 8 profile visitors
with relative timestamps - and ONLY the owner ever sees the card.
* Privacy first: a "👻 Browse profiles invisibly" switch in Settings keeps
a member out of everyone's visitor lists entirely.
🎂 BIRTHDAYS
* Members set month + day in Settings (validated; year never asked).
On the day: a token gift (amount yours to set), a site notification,
and a spot on the homepage "Today's Birthdays" card. Gifted exactly
once per year, race-proof.
v6.9.9
Token grants that silently skipped now land, ledgered, on every edition
* FIXED - Donor monthly token allotments never actually credited: the sweep
that grants them runs during daily maintenance, BEFORE mods load, so the
wallet function it looked for was never there and the grant silently
skipped - every day, on every edition. And in configurations where a
credit did land, the sweep's own save could immediately overwrite it.
Donors with an allotment configured now receive it, once per month, with
a proper wallet-ledger entry.
* NEW under the hood - one canonical core token mover (used by the fix
above): every token movement runs under the same exclusive lock and
lands in the same tamper-evident ledger hash chain, on every edition,
in every context - no more "if this mod happens to be loaded" wallet
roulette.
* Daily login streak rewards and invite rewards now use it too: on
installs without the store or Pet World mods they used to credit tokens
with NO ledger entry at all - invisible in wallet history, skipped by
the hash chain. Every reward is now recorded like any other transaction.
that grants them runs during daily maintenance, BEFORE mods load, so the
wallet function it looked for was never there and the grant silently
skipped - every day, on every edition. And in configurations where a
credit did land, the sweep's own save could immediately overwrite it.
Donors with an allotment configured now receive it, once per month, with
a proper wallet-ledger entry.
* NEW under the hood - one canonical core token mover (used by the fix
above): every token movement runs under the same exclusive lock and
lands in the same tamper-evident ledger hash chain, on every edition,
in every context - no more "if this mod happens to be loaded" wallet
roulette.
* Daily login streak rewards and invite rewards now use it too: on
installs without the store or Pet World mods they used to credit tokens
with NO ledger entry at all - invisible in wallet history, skipped by
the hash chain. Every reward is now recorded like any other transaction.
v6.9.8
Hardening pass: race-proof store ownership + per-domain mail servers
* Store (v2.9.1) - every change to a member's record (item grants, add-on
toggles, collection cleanup, the trophy-shelf switch, admin gifts) now
runs under the same exclusive file lock the token wallet has used since
v6.7.8. Before this, a purchase landing at the same instant as a chat
heartbeat, a poker payout or the daily login bonus could silently lose
one of the two writes - a member could pay and not receive the item.
* Store - double-charge protection: two rapid taps on Buy (or two
simultaneous gifts of the same item) could each take payment before
either recorded ownership. The duplicate is now detected inside the
lock and refunds itself in full, tokens and seller cut both reversed,
with a clear "already own this - tokens refunded" message.
* Email Mailboxes (v1.3.3) - each mailbox domain now connects to its OWN
mail.<domain> server for reading and sending. Previously, once a cPanel
host was configured it was used for EVERY domain - which silently broke
webmail for any extra domain hosted on a different server. The primary
domain still uses your cPanel host when set.
* Full platform audit alongside these fixes: every button, form and page
link cross-referenced against its handler (no dead ends found), cam-room
signaling endpoints and tile rules verified, all new endpoints CSRF
checked, all output paths sanitized.
toggles, collection cleanup, the trophy-shelf switch, admin gifts) now
runs under the same exclusive file lock the token wallet has used since
v6.7.8. Before this, a purchase landing at the same instant as a chat
heartbeat, a poker payout or the daily login bonus could silently lose
one of the two writes - a member could pay and not receive the item.
* Store - double-charge protection: two rapid taps on Buy (or two
simultaneous gifts of the same item) could each take payment before
either recorded ownership. The duplicate is now detected inside the
lock and refunds itself in full, tokens and seller cut both reversed,
with a clear "already own this - tokens refunded" message.
* Email Mailboxes (v1.3.3) - each mailbox domain now connects to its OWN
mail.<domain> server for reading and sending. Previously, once a cPanel
host was configured it was used for EVERY domain - which silently broke
webmail for any extra domain hosted on a different server. The primary
domain still uses your cPanel host when set.
* Full platform audit alongside these fixes: every button, form and page
link cross-referenced against its handler (no dead ends found), cam-room
signaling endpoints and tile rules verified, all new endpoints CSRF
checked, all output paths sanitized.
v6.9.7
Store: gifting actually works + the 🏆 Trophy Shelf arrives (store v2.9.0)
* FIXED - gifting was advertised but broken: the "gift to a friend" form on
every store item page was invisible for everyone. It built its friends
list from a friends.json file this platform never creates (friendships
live on the member record), so the list always came back empty and the
form never rendered. It now reads the real friends list - members with
friends see "🎁 Gift this to a friend" on every approved item, pick a
friend (alphabetized), and pay the tokens; the item lands in the
friend's collection with a notification. You can gift items you
already own yourself.
* NEW - 🏆 Trophy Shelf: every member's profile now shows a "Collection"
card of the store items they own (up to 12 icons + a "+N more" count).
Every icon links straight to that item's store page, where visitors can
buy it - or gift it - themselves. Social proof that quietly markets
your store, exactly as promised on the tin.
* Members control it: a toggle at the top of My Add-ons shows or hides
their shelf (shown by default). Removed and pending items never appear.
every store item page was invisible for everyone. It built its friends
list from a friends.json file this platform never creates (friendships
live on the member record), so the list always came back empty and the
form never rendered. It now reads the real friends list - members with
friends see "🎁 Gift this to a friend" on every approved item, pick a
friend (alphabetized), and pay the tokens; the item lands in the
friend's collection with a notification. You can gift items you
already own yourself.
* NEW - 🏆 Trophy Shelf: every member's profile now shows a "Collection"
card of the store items they own (up to 12 icons + a "+N more" count).
Every icon links straight to that item's store page, where visitors can
buy it - or gift it - themselves. Social proof that quietly markets
your store, exactly as promised on the tin.
* Members control it: a toggle at the top of My Add-ons shows or hides
their shelf (shown by default). Removed and pending items never appear.
v6.9.6
Members can turn OFF all site animation (new setting)
* New per-member kill switch: Settings -> 🔔 Notifications & Privacy ->
"🎬 Display & motion" -> Turn off site animations. Great for motion
sensitivity, battery life, and older devices.
* Flipping it on stops the animated background EVERYWHERE for that member -
the site default, their own Edit Profile pick, and even per-room
background overrides - and freezes all other moving effects (gradients,
pulses, hover transitions) across the site, for their view only.
* Built the safe way: animation durations are clamped to ~0 instead of
removed, so anything that waits for an animation to finish keeps working.
Games and video are untouched - it only stills the decoration.
* The switch always works, even if you've disabled "let members pick their
own animated background" - turning motion off is treated as an
accessibility choice, not a customization perk.
"🎬 Display & motion" -> Turn off site animations. Great for motion
sensitivity, battery life, and older devices.
* Flipping it on stops the animated background EVERYWHERE for that member -
the site default, their own Edit Profile pick, and even per-room
background overrides - and freezes all other moving effects (gradients,
pulses, hover transitions) across the site, for their view only.
* Built the safe way: animation durations are clamped to ~0 instead of
removed, so anything that waits for an animation to finish keeps working.
Games and video are untouched - it only stills the decoration.
* The switch always works, even if you've disabled "let members pick their
own animated background" - turning motion off is treated as an
accessibility choice, not a customization perk.
v6.9.5
Mailbox claim page: the address box is actually visible now (mod v1.3.2)
* The "pick your address" field on the claim page could render completely
invisible - no box, no text, nowhere to type - on themes that don't
define the input color variables. The name + @domain picker now carries
its own visible styling (outlined box, readable text and placeholder,
readable dropdown) on EVERY theme, and only inherits your theme's input
colors when the theme actually provides them.
* Domain list cleanup: the @-dropdown now offers exactly the domains you
configure in Admin -> ✉️ Email Mailboxes (primary + extra domains) -
nothing hard-coded. Reminder: for instant auto-creation, every domain
must be an addon/parked domain on the SAME cPanel account as your saved
credentials. A domain hosted on a different account still works, but
each claim on it arrives as a manual request you fulfill by hand.
invisible - no box, no text, nowhere to type - on themes that don't
define the input color variables. The name + @domain picker now carries
its own visible styling (outlined box, readable text and placeholder,
readable dropdown) on EVERY theme, and only inherits your theme's input
colors when the theme actually provides them.
* Domain list cleanup: the @-dropdown now offers exactly the domains you
configure in Admin -> ✉️ Email Mailboxes (primary + extra domains) -
nothing hard-coded. Reminder: for instant auto-creation, every domain
must be an addon/parked domain on the SAME cPanel account as your saved
credentials. A domain hosted on a different account still works, but
each claim on it arrives as a manual request you fulfill by hand.
v6.9.4
Email Mailboxes: members can actually find them now (mod v1.3.1)
* Fixed a dead end in the mailbox mod: the nav link only appeared for
members who ALREADY had an active mailbox - so a member without one had
no path to the claim page at all (short of hand-typing ?page=mailbox).
Members without a mailbox now see "✉️ Get Email" in the nav, which
opens the claim page.
* Once claimed, the link reads "✉️ Mailbox" - while the mailbox is
pending manual setup it opens the status page, and once active it opens
webmail exactly as before.
* Donors-only sites: members who aren't donors yet see the link too and
land on the "upgrade your membership to claim one" card - that upsell
page existed all along but was unreachable.
* No settings changed. The mailbox domain plus any extra addon domains
still live in Admin -> ✉️ Email Mailboxes.
members who ALREADY had an active mailbox - so a member without one had
no path to the claim page at all (short of hand-typing ?page=mailbox).
Members without a mailbox now see "✉️ Get Email" in the nav, which
opens the claim page.
* Once claimed, the link reads "✉️ Mailbox" - while the mailbox is
pending manual setup it opens the status page, and once active it opens
webmail exactly as before.
* Donors-only sites: members who aren't donors yet see the link too and
land on the "upgrade your membership to claim one" card - that upsell
page existed all along but was unreachable.
* No settings changed. The mailbox domain plus any extra addon domains
still live in Admin -> ✉️ Email Mailboxes.
v6.9.3
The 24-hour logout is dead
* Found and fixed the thing logging members out after a day: a hard-coded
24-hour session wall that stamped your login time once and never
refreshed it - so exactly 24h after logging in you were dumped MID-USE,
no matter how active you were.
* Login sessions are now a rolling IDLE window: any activity refreshes it,
so an active member is never logged out. Default 30 days of inactivity.
* Admin-tunable on the ⏱️ Room Sessions tab ("Login sessions" card),
1-365 days. The session cookie and server-side lifetimes now match the
setting too, so neither the browser nor PHP expires you early.
* Unrelated to the room session caps above - those only limit time IN A
ROOM (and are off by default); this is about staying signed in.
24-hour session wall that stamped your login time once and never
refreshed it - so exactly 24h after logging in you were dumped MID-USE,
no matter how active you were.
* Login sessions are now a rolling IDLE window: any activity refreshes it,
so an active member is never logged out. Default 30 days of inactivity.
* Admin-tunable on the ⏱️ Room Sessions tab ("Login sessions" card),
1-365 days. The session cookie and server-side lifetimes now match the
setting too, so neither the browser nor PHP expires you early.
* Unrelated to the room session caps above - those only limit time IN A
ROOM (and are off by default); this is about staying signed in.
v6.9.2
Room session time controls (new admin tab)
* New Admin -> ⏱️ Room Sessions tab: cap how long a member can stay in a
room in one sitting, site-wide. Optional rejoin cooldown makes them take
a real break before entering any room again. Both OFF by default
(0 = unlimited) - flip them on only if you want them.
* Enforcement is server-side on the room heartbeat: when the cap hits, the
member gets a clear message, is dropped from the room, and lands on the
Browse page. The room page and the join API both honor the cooldown, so
there's no sneaking back in early.
* Staff exemption (on by default): site admins, room owners and room mods
are never timed out of rooms they run.
* The tab also lists everyone currently cooling down, with per-member and
clear-all release buttons.
room in one sitting, site-wide. Optional rejoin cooldown makes them take
a real break before entering any room again. Both OFF by default
(0 = unlimited) - flip them on only if you want them.
* Enforcement is server-side on the room heartbeat: when the cap hits, the
member gets a clear message, is dropped from the room, and lands on the
Browse page. The room page and the join API both honor the cooldown, so
there's no sneaking back in early.
* Staff exemption (on by default): site admins, room owners and room mods
are never timed out of rooms they run.
* The tab also lists everyone currently cooling down, with per-member and
clear-all release buttons.
v6.9.1
Registration rate limit: fixed and in your hands
* Fixed the "too many registration attempts" wall that could appear after a
single failed try. The limit was being checked TWICE per submit (with two
different caps, 5/hr and 3/hr) and every check counts as an attempt - so
one typo burned two of three slots and the very next submit was blocked
for an hour. It's one check now.
* The numbers are yours: Admin -> Anti-Spam -> "Registration attempts per
IP ... per ... minutes". Default 5 per 60 minutes. Every submit counts
(including typo retries), so don't set it to 1.
single failed try. The limit was being checked TWICE per submit (with two
different caps, 5/hr and 3/hr) and every check counts as an attempt - so
one typo burned two of three slots and the very next submit was blocked
for an hour. It's one check now.
* The numbers are yours: Admin -> Anti-Spam -> "Registration attempts per
IP ... per ... minutes". Default 5 per 60 minutes. Every submit counts
(including typo retries), so don't set it to 1.
v6.9.0
Poker: steady raise slider + ALL IN (mod v1.5.0)
* The raise slider no longer snaps back to minimum while you're using it.
The table repaints every couple of seconds and used to reset the slider
each time; it now remembers your chosen amount for the current street,
restores it after every repaint, and while your finger is actually ON
the slider the repaint waits entirely - nothing yanks it mid-drag.
* New 🔥 ALL IN button, right where it belongs. One tap (with a confirm)
pushes your whole stack - and it's poker-correct: going all-in is legal
even when you can't cover the minimum raise, exactly like a real table.
Shows the exact amount you're shoving before you commit.
The table repaints every couple of seconds and used to reset the slider
each time; it now remembers your chosen amount for the current street,
restores it after every repaint, and while your finger is actually ON
the slider the repaint waits entirely - nothing yanks it mid-drag.
* New 🔥 ALL IN button, right where it belongs. One tap (with a confirm)
pushes your whole stack - and it's poker-correct: going all-in is legal
even when you can't cover the minimum raise, exactly like a real table.
Shows the exact amount you're shoving before you commit.
v6.8.9
Username rules unstrangled + new Anti-Spam admin tab
* Numbers in usernames are now ALWAYS welcome - 420, 69, 67, Maddog420,
whatever. The old "recognizable username" check misread leetspeak and
number-heavy names as bot mash and rejected real people; it now decodes
leet first (th3d4rkkn1ght reads as thedarkknight) and only flags long,
genuinely unpronounceable keyboard mash. xX_...Xx styling is fine too.
* Banned-word matching for USERNAMES switched from substring to whole-word:
an "ass" entry no longer blocks Cassie, Grass420, Passion or B4ssHead -
a listed word only hits when it stands on its own. (Chat filtering keeps
the stricter substring behavior.)
* New Admin -> 🚫 Anti-Spam tab collects every gate in one place: honeypot,
time gate, Gmail-alias normalizing, disposable-email block, Tor block,
the username check, math captcha, first-posts review, plus the banned
words / disposable domains / Tor lists (moved from Moderation).
* Includes a "Test a username" tool: type any name and see exactly which
gate would block it - and why - before a member ever hits the wall.
* Usernames can no longer shadow page URLs (poker, feed, bookmarks, etc.
joined the reserved list) - that used to quietly break a member's own
profile link.
whatever. The old "recognizable username" check misread leetspeak and
number-heavy names as bot mash and rejected real people; it now decodes
leet first (th3d4rkkn1ght reads as thedarkknight) and only flags long,
genuinely unpronounceable keyboard mash. xX_...Xx styling is fine too.
* Banned-word matching for USERNAMES switched from substring to whole-word:
an "ass" entry no longer blocks Cassie, Grass420, Passion or B4ssHead -
a listed word only hits when it stands on its own. (Chat filtering keeps
the stricter substring behavior.)
* New Admin -> 🚫 Anti-Spam tab collects every gate in one place: honeypot,
time gate, Gmail-alias normalizing, disposable-email block, Tor block,
the username check, math captcha, first-posts review, plus the banned
words / disposable domains / Tor lists (moved from Moderation).
* Includes a "Test a username" tool: type any name and see exactly which
gate would block it - and why - before a member ever hits the wall.
* Usernames can no longer shadow page URLs (poker, feed, bookmarks, etc.
joined the reserved list) - that used to quietly break a member's own
profile link.
v6.8.8
Poker "Loading table..." SOLVED (mod v1.4.1)
* Found the actual root cause of tables freezing on "Loading table...":
a deck refactor converted cards to numeric indices and updated every
display path except one - each player's OWN hole cards were sent raw.
The table renderer choked on them the moment a hand was dealt, so any
table with cards in the air froze on the loading message forever. The
lobby, table creation and empty tables all worked, which made it look
random. One line, fixed - cards render everywhere again.
* The table screen also got armor so nothing can silently freeze it
again: connection problems, server hiccups and display errors now show
a clear message right on the felt and keep retrying instead of hanging.
* If you grabbed v6.8.7 earlier: this build supersedes it - deploy this
one.
a deck refactor converted cards to numeric indices and updated every
display path except one - each player's OWN hole cards were sent raw.
The table renderer choked on them the moment a hand was dealt, so any
table with cards in the air froze on the loading message forever. The
lobby, table creation and empty tables all worked, which made it look
random. One line, fixed - cards render everywhere again.
* The table screen also got armor so nothing can silently freeze it
again: connection problems, server hiccups and display errors now show
a clear message right on the felt and keep retrying instead of hanging.
* If you grabbed v6.8.7 earlier: this build supersedes it - deploy this
one.
v6.8.7
Poker Room bulletproofing (mod v1.4.0)
* Fixed the classic "tables won't load" failure: if anything on the server
leaked a PHP notice into a poker response (one odd legacy table row was
enough on hosts with display_errors on), the lobby spun forever. Every
poker JSON reply is now scrubbed clean before it's sent.
* One broken table can no longer freeze the room: a table that errors
mid-game is automatically quarantined - every human's chips are banked
back to their wallet, the table is removed, the player lands safely back
in the lobby, and the exact table id is written to the PHP error log.
* Structurally-damaged rows in pkr_tables.json (interrupted writes,
hand-edits, ancient leftovers) are silently repaired out instead of
crashing the lobby.
* "Clear all tables" in the poker admin now banks every seated player's
stack BEFORE wiping - previously it silently destroyed any chips still
sitting on a table.
leaked a PHP notice into a poker response (one odd legacy table row was
enough on hosts with display_errors on), the lobby spun forever. Every
poker JSON reply is now scrubbed clean before it's sent.
* One broken table can no longer freeze the room: a table that errors
mid-game is automatically quarantined - every human's chips are banked
back to their wallet, the table is removed, the player lands safely back
in the lobby, and the exact table id is written to the PHP error log.
* Structurally-damaged rows in pkr_tables.json (interrupted writes,
hand-edits, ancient leftovers) are silently repaired out instead of
crashing the lobby.
* "Clear all tables" in the poker admin now banks every seated player's
stack BEFORE wiping - previously it silently destroyed any chips still
sitting on a table.
v6.8.6
Achievements are ALIVE
* The achievement system that shipped dormant in early builds is now fully
wired: 13 badges covering chat, friends, bulletins, bookmarks, stories,
account age, likes received and supporting the site as a donor.
* Earned badges show on the member's profile (hover any badge for how it
was earned). Fresh unlocks pop a celebratory toast plus a 🏆 notification
the moment they happen - checked automatically, nothing to run.
* Members who somehow earned old-system badges keep them; every legacy
badge ID carried over untouched.
* Toggle: Admin -> Settings -> Site Features -> "Achievements". ON by
default.
wired: 13 badges covering chat, friends, bulletins, bookmarks, stories,
account age, likes received and supporting the site as a donor.
* Earned badges show on the member's profile (hover any badge for how it
was earned). Fresh unlocks pop a celebratory toast plus a 🏆 notification
the moment they happen - checked automatically, nothing to run.
* Members who somehow earned old-system badges keep them; every legacy
badge ID carried over untouched.
* Toggle: Admin -> Settings -> Site Features -> "Achievements". ON by
default.
v6.8.5
RSS feed for the bulletin board
* Your site now serves a standard RSS 2.0 feed at ?action=rss - the latest
30 live bulletins, auto-discovered by feed readers via the page <head>.
* Privacy-safe by design: the feed carries titles, authors, dates and
permalinks only - bulletin BODIES never leave the site (same rule as the
share previews). Pending-review and scheduled bulletins never appear.
* Toggle: Admin -> Settings -> Site Features -> "Public RSS feed". ON by
default.
30 live bulletins, auto-discovered by feed readers via the page <head>.
* Privacy-safe by design: the feed carries titles, authors, dates and
permalinks only - bulletin BODIES never leave the site (same rule as the
share previews). Pending-review and scheduled bulletins never appear.
* Toggle: Admin -> Settings -> Site Features -> "Public RSS feed". ON by
default.
v6.8.4
Bookmarks + multi-domain mailboxes
* Mailbox mod v1.3.0: a new "Extra mailbox domains" field in the mailbox
admin settings. List extra domains (comma-separated) that live on the
same cPanel account and they all join the @-dropdown members pick from
when claiming their email address. One set of credentials provisions
every domain.
admin settings. List extra domains (comma-separated) that live on the
same cPanel account and they all join the @-dropdown members pick from
when claiming their email address. One set of credentials provisions
every domain.
v6.8.4
Bookmarks: save bulletins for later
* Every bulletin now carries a 🔖 Save button (on the board and on its
permalink page). One tap saves it; saved bulletins live on a new
Bookmarks page under the Community menu, newest saves first, complete
with link previews and one-tap unsave.
* Private to each member, self-cleaning (bookmarks to deleted bulletins
quietly disappear), and capped at a roomy 500 per member.
permalink page). One tap saves it; saved bulletins live on a new
Bookmarks page under the Community menu, newest saves first, complete
with link previews and one-tap unsave.
* Private to each member, self-cleaning (bookmarks to deleted bulletins
quietly disappear), and capped at a roomy 500 per member.
v6.8.3
Schedule bulletins for later
* The bulletin composer grew a tucked-away "Schedule for later" field: pick
a date & time (in YOUR timezone - the site converts) and the bulletin
publishes itself on the dot. No cron jobs, nothing to configure.
* Until it goes live, a scheduled bulletin is visible only to its author -
a "Your scheduled bulletins" strip on the board shows what's queued, when
each goes live, and a one-tap cancel. Live feeds pick a bulletin up the
moment its time arrives.
* Tidy limits: schedule from 1 minute to 1 year out, up to 20 queued per
member. Admin review rules still apply exactly as before.
* Also fixed along the way: bulletins pending review no longer surface in
site search results.
a date & time (in YOUR timezone - the site converts) and the bulletin
publishes itself on the dot. No cron jobs, nothing to configure.
* Until it goes live, a scheduled bulletin is visible only to its author -
a "Your scheduled bulletins" strip on the board shows what's queued, when
each goes live, and a one-tap cancel. Live feeds pick a bulletin up the
moment its time arrives.
* Tidy limits: schedule from 1 minute to 1 year out, up to 20 queued per
member. Admin review rules still apply exactly as before.
* Also fixed along the way: bulletins pending review no longer surface in
site search results.
v6.8.2
Real browser push notifications
* Members can now get a real device popup for private messages, friend
requests and @mentions - even when the site's tab is CLOSED. Opt-in per
member and per device: Settings -> Notifications -> "Enable on this
device". Works on desktop and Android out of the box; on iPhone/iPad the
member adds the site to their Home Screen first (standard for web push).
* Zero setup for you and zero third parties: the push keys generate
themselves on first use, notification text never travels through the
push relay (the site's own service worker fetches it with the member's
session), and dead device registrations clean themselves up. Needs HTTPS
and PHP's OpenSSL + cURL (both standard).
* The site now ships a service worker, quietly registered for logged-in
members - the foundation for push today and installable-app behavior
down the road.
requests and @mentions - even when the site's tab is CLOSED. Opt-in per
member and per device: Settings -> Notifications -> "Enable on this
device". Works on desktop and Android out of the box; on iPhone/iPad the
member adds the site to their Home Screen first (standard for web push).
* Zero setup for you and zero third parties: the push keys generate
themselves on first use, notification text never travels through the
push relay (the site's own service worker fetches it with the member's
session), and dead device registrations clean themselves up. Needs HTTPS
and PHP's OpenSSL + cURL (both standard).
* The site now ships a service worker, quietly registered for logged-in
members - the foundation for push today and installable-app behavior
down the road.
v6.8.1
Link preview cards in bulletins and chat
* Paste a link and a compact preview card grows under it - site name, page
title, description and thumbnail - in bulletins (board, Home and the
permalink page) and in chat rooms, just like modern messengers.
* Built safe and light: server-side fetch locked to public websites only
(private/internal addresses are refused), 400KB read cap, short timeouts,
and a 7-day cache so each link is fetched once for the whole site. Rate
limited per member.
* One switch to turn it off site-wide: Admin -> Settings -> Site Features ->
"Show link preview cards". ON by default.
title, description and thumbnail - in bulletins (board, Home and the
permalink page) and in chat rooms, just like modern messengers.
* Built safe and light: server-side fetch locked to public websites only
(private/internal addresses are refused), 400KB read cap, short timeouts,
and a 7-day cache so each link is fetched once for the whole site. Rate
limited per member.
* One switch to turn it off site-wide: Admin -> Settings -> Site Features ->
"Show link preview cards". ON by default.
v6.8.0
Every bulletin is now shareable
* Every bulletin has its own PERMALINK page with the full share-button row
(Facebook, X, WhatsApp, Telegram, Reddit, email, copy-link and the native
share sheet) plus the like button - so a great post can travel.
* Shared bulletin links unfurl into a proper preview card on social apps:
title, author and their avatar. Privacy stays intact - the body text is
NEVER exposed to link crawlers; visitors are invited to log in to read.
* Timestamps on the bulletin board and the Home page's Friends' Bulletins
now link to the permalink, activity-feed entries link straight to the
bulletin, and site search results open the exact bulletin instead of the
board.
(Facebook, X, WhatsApp, Telegram, Reddit, email, copy-link and the native
share sheet) plus the like button - so a great post can travel.
* Shared bulletin links unfurl into a proper preview card on social apps:
title, author and their avatar. Privacy stays intact - the body text is
NEVER exposed to link crawlers; visitors are invited to log in to read.
* Timestamps on the bulletin board and the Home page's Friends' Bulletins
now link to the permalink, activity-feed entries link straight to the
bulletin, and site search results open the exact bulletin instead of the
board.
v6.7.9
Maintenance: link-sharing abuse protection
* Hardened the flagship network's short-link service against spam farming:
per-member creation limits and an admin domain blocklist. Blocking a
domain refuses new links to it AND instantly disables every existing
short link pointing at it. (Infrastructure release - runs on the flagship
network; nothing changes on buyer sites in this version.)
per-member creation limits and an admin domain blocklist. Blocking a
domain refuses new links to it AND instantly disables every existing
short link pointing at it. (Infrastructure release - runs on the flagship
network; nothing changes on buyer sites in this version.)
v6.7.8
Token wallet made bulletproof under load
* Every token movement (poker payouts, store buys, tips, pet purchases,
admin grants) now runs inside a single exclusive lock, so two things
hitting your balance at the same instant can never overwrite each other.
Before this, a rare mid-payout collision could silently drop one change.
* The tamper-evident wallet ledger switched to an append-only journal.
Same SHA-256 hash chain, but each transaction is one tiny append instead
of rewriting the whole history file - token actions stay instant no
matter how long your site has been running. Your existing ledger history
is folded in automatically the first time the new build runs (the old
file is kept on disk as wallet_ledger.legacy.json, just in case).
* Fixed: admin item GIFTS used to write a raw, unchained ledger row that
broke the hash chain's verify. Gifts are now chained like everything else.
admin grants) now runs inside a single exclusive lock, so two things
hitting your balance at the same instant can never overwrite each other.
Before this, a rare mid-payout collision could silently drop one change.
* The tamper-evident wallet ledger switched to an append-only journal.
Same SHA-256 hash chain, but each transaction is one tiny append instead
of rewriting the whole history file - token actions stay instant no
matter how long your site has been running. Your existing ledger history
is folded in automatically the first time the new build runs (the old
file is kept on disk as wallet_ledger.legacy.json, just in case).
* Fixed: admin item GIFTS used to write a raw, unchained ledger row that
broke the hash chain's verify. Gifts are now chained like everything else.
v6.7.7
How-to guides on every game, more store items, smoother Mad City
* NEW "How to play?" guide tucked under every game's table or board - poker
and the card-room games (Blackjack, Three Card Poker, Baccarat, War), the
arcade board games (Connect Four, Checkers, Reversi, Gomoku, Tic-Tac-Toe,
Battleship, Chess), the puzzles (Sudoku, Word Guess, Word Search, Anagram),
and the casino games. Collapsed by default so it never gets in a regular's
way - and right there the moment a newcomer needs it.
* Poker tables: whoever starts a table now picks how many computer players
sit down with them (None, 1, 3 or 5) right in the lobby. Real players always
get a seat, so a busy table never locks anyone out.
* 13 new ready-to-sell store items across every category - two themes, two
animated banners, two full-screen overlays, a metallic name effect, a hover
effect, a cursor, a mouse trail, an arcade entrance sound, a galaxy chat-flair
badge, and a new "Telephone" cam-room voice filter. The store now ships
stocked with 68 items.
* Mad City loads faster and runs smoother on every machine: it now starts on
its lightweight graphics settings by default instead of starting heavy and
visibly dropping quality mid-play.
and the card-room games (Blackjack, Three Card Poker, Baccarat, War), the
arcade board games (Connect Four, Checkers, Reversi, Gomoku, Tic-Tac-Toe,
Battleship, Chess), the puzzles (Sudoku, Word Guess, Word Search, Anagram),
and the casino games. Collapsed by default so it never gets in a regular's
way - and right there the moment a newcomer needs it.
* Poker tables: whoever starts a table now picks how many computer players
sit down with them (None, 1, 3 or 5) right in the lobby. Real players always
get a seat, so a busy table never locks anyone out.
* 13 new ready-to-sell store items across every category - two themes, two
animated banners, two full-screen overlays, a metallic name effect, a hover
effect, a cursor, a mouse trail, an arcade entrance sound, a galaxy chat-flair
badge, and a new "Telephone" cam-room voice filter. The store now ships
stocked with 68 items.
* Mad City loads faster and runs smoother on every machine: it now starts on
its lightweight graphics settings by default instead of starting heavy and
visibly dropping quality mid-play.
v6.7.6
Paste-to-upload in chat + privacy hardening
* Paste an image straight into the chat box - a screenshot, a copied picture,
or an image file on your clipboard - and it uploads to the room instantly,
just like Imgur. No need to open the file picker. Same 5 MB limit and the
same allowed image types as the picker; plain-text and GIF-link pastes work
exactly as before.
* Contact details on your public pages (Brand, Privacy, Terms) now come from
YOUR own site settings - your admin email, or an address derived from your
own domain - and the legal/DMCA agent name + address are settings you fill
in (the address line stays hidden until you do). No other site's details are
ever shown on yours.
or an image file on your clipboard - and it uploads to the room instantly,
just like Imgur. No need to open the file picker. Same 5 MB limit and the
same allowed image types as the picker; plain-text and GIF-link pastes work
exactly as before.
* Contact details on your public pages (Brand, Privacy, Terms) now come from
YOUR own site settings - your admin email, or an address derived from your
own domain - and the legal/DMCA agent name + address are settings you fill
in (the address line stays hidden until you do). No other site's details are
ever shown on yours.
v6.7.5
Marketplace Mode + a bigger Marketplace + more animated backgrounds
* NEW "Marketplace Mode" (Admin -> Settings -> Site Features). Flip one switch
and your site becomes a marketplace-first community instead of a chat-first
one: the front door (your bare domain) opens on the Marketplace, and a
prominent Marketplace link leads the top menu. It's OFF by default - nothing
changes until you turn it on - and everything else (chat rooms, profiles,
the feed) still works exactly as before, just one tap away. Turn it back off
any time to restore the normal chat-community layout.
* The Marketplace grew up. It now does almost everything a standalone store
script does, member-to-member:
- Auctions: list with a starting bid, bid increment and end time; live
high-bid tracking, bid history, anti-snipe auto-extend, and an optional
Buy-It-Now price to end early.
- Make an Offer: buyers can send a price; sellers accept, decline or
counter, all from the listing.
- Ratings & reviews: leave a seller a star rating and a note after a deal;
every seller gets an average score shown on their listings and store.
- Watchlist: heart any listing to save it; a "Watching" page keeps them
together and shows how many people are watching each item.
- Saved searches: save a search + category and get notified the moment a
new matching listing is posted.
- Seller storefronts: every member gets a public shop page with all their
active listings and their rating.
- Sort & filter: newest, price low/high, ending soon, most-watched, plus a
condition filter and price range.
- Mark as Sold, quantity/stock, and a one-tap Report button for admins.
Existing listings carry over untouched - all the new fields are optional.
* Added more animated backgrounds (Admin -> Settings has the live preview):
cherry-blossom drift, autumn leaves, lava-lamp blobs, deep-space nebula,
rainfall, drifting fog, hyperspace warp, and a money-rain theme that pairs
nicely with Marketplace Mode. Members can still pick their own.
* Version stamping cleanup: the installer and setup pages now always show the
current version on every tier.
and your site becomes a marketplace-first community instead of a chat-first
one: the front door (your bare domain) opens on the Marketplace, and a
prominent Marketplace link leads the top menu. It's OFF by default - nothing
changes until you turn it on - and everything else (chat rooms, profiles,
the feed) still works exactly as before, just one tap away. Turn it back off
any time to restore the normal chat-community layout.
* The Marketplace grew up. It now does almost everything a standalone store
script does, member-to-member:
- Auctions: list with a starting bid, bid increment and end time; live
high-bid tracking, bid history, anti-snipe auto-extend, and an optional
Buy-It-Now price to end early.
- Make an Offer: buyers can send a price; sellers accept, decline or
counter, all from the listing.
- Ratings & reviews: leave a seller a star rating and a note after a deal;
every seller gets an average score shown on their listings and store.
- Watchlist: heart any listing to save it; a "Watching" page keeps them
together and shows how many people are watching each item.
- Saved searches: save a search + category and get notified the moment a
new matching listing is posted.
- Seller storefronts: every member gets a public shop page with all their
active listings and their rating.
- Sort & filter: newest, price low/high, ending soon, most-watched, plus a
condition filter and price range.
- Mark as Sold, quantity/stock, and a one-tap Report button for admins.
Existing listings carry over untouched - all the new fields are optional.
* Added more animated backgrounds (Admin -> Settings has the live preview):
cherry-blossom drift, autumn leaves, lava-lamp blobs, deep-space nebula,
rainfall, drifting fog, hyperspace warp, and a money-rain theme that pairs
nicely with Marketplace Mode. Members can still pick their own.
* Version stamping cleanup: the installer and setup pages now always show the
current version on every tier.
v6.7.4
Klout demo fix + poker notice fix
* Klout: the demo accounts now drop off the leaderboard immediately. The board
filters them out at display time and a one-time rebuild clears them from the
cached scores, so they no longer linger until the cache expires.
* Card Room: fixed a PHP notice ("Only variables should be passed by
reference") that appeared in the poker lobby / create / sit actions.
filters them out at display time and a one-time rebuild clears them from the
cached scores, so they no longer linger until the cache expires.
* Card Room: fixed a PHP notice ("Only variables should be passed by
reference") that appeared in the poker lobby / create / sit actions.
v6.7.3
Live updates + Klout demo cleanup
* The Activity Feed now updates live - new activity slides in at the top on
its own, no page refresh needed. The "Online Now" widget also updates in
place (members appear/disappear smoothly) instead of reloading the page.
* Klout now leaves the seeded demo accounts (demo_user, demo_admin) off the
leaderboard and scoring, so they don't clutter the influencer board. Which
accounts are excluded is editable in the Klout admin settings.
its own, no page refresh needed. The "Online Now" widget also updates in
place (members appear/disappear smoothly) instead of reloading the page.
* Klout now leaves the seeded demo accounts (demo_user, demo_admin) off the
leaderboard and scoring, so they don't clutter the influencer board. Which
accounts are excluded is editable in the Klout admin settings.
v6.7.2
Sudoku tap controls
* Reworked Sudoku selection so tapping a square reliably highlights it in gold
(the whole grid now uses a single tap handler that works on phones and
desktop alike). Tap a square, then tap a number to fill it; arrow keys and
typing still work on a keyboard. Fixes taps/arrows not moving the selection.
(the whole grid now uses a single tap handler that works on phones and
desktop alike). Tap a square, then tap a number to fill it; arrow keys and
typing still work on a keyboard. Fixes taps/arrows not moving the selection.
v6.7.1
Sudoku controls fix
* Sudoku now has clear cell controls: move around the grid with the arrow
keys (or tap a square), with a brighter highlight on the selected cell and
a short how-to-play hint. Matching numbers light up across the board. Fixes
the puzzle feeling like there was no way to enter or move between squares.
keys (or tap a square), with a brighter highlight on the selected cell and
a short how-to-play hint. Matching numbers light up across the board. Fixes
the puzzle feeling like there was no way to enter or move between squares.
v6.7.0
Card Room: poker now pays from the token wallet + new casino games
* The Poker Room is now a full Card Room. Hold'em chips are the site's shared
token wallet (the same balance the casino, store and pet world use) instead
of separate play-money - buy-in debits your tokens and leaving banks them
back. Tables now close out properly: your stack is always returned to your
wallet on leave, bust or when an abandoned table is reaped, so chips can
never be stranded.
* Added four house games alongside Hold'em - Blackjack (3:2, dealer stands on
17, double), Three Card Poker (ante + Pair Plus), Baccarat (player / banker
with 5% commission / tie) and War. All server-authoritative (the server owns
every card) and settled through the same wallet.
* Six selectable table themes (felt + rail colours), per player, with an admin
default. Admins can also set the card-room bet limits.
token wallet (the same balance the casino, store and pet world use) instead
of separate play-money - buy-in debits your tokens and leaving banks them
back. Tables now close out properly: your stack is always returned to your
wallet on leave, bust or when an abandoned table is reaped, so chips can
never be stranded.
* Added four house games alongside Hold'em - Blackjack (3:2, dealer stands on
17, double), Three Card Poker (ante + Pair Plus), Baccarat (player / banker
with 5% commission / tie) and War. All server-authoritative (the server owns
every card) and settled through the same wallet.
* Six selectable table themes (felt + rail colours), per player, with an admin
default. Admins can also set the card-room bet limits.
v6.6.9
Poker auto-fills with AI opponents
* The Poker Room now auto-fills empty seats with AI opponents the moment you
sit down, so you get a full table and a live game instantly without waiting
for other people and without pressing any buttons. Busted AI players rebuy
on their own so the table never empties out mid-session. Admins can set how
many AI opponents fill each table (or switch auto-fill off) in the Poker
Room settings. Removed the old manual add/clear-bot buttons.
sit down, so you get a full table and a live game instantly without waiting
for other people and without pressing any buttons. Busted AI players rebuy
on their own so the table never empties out mid-session. Admins can set how
many AI opponents fill each table (or switch auto-fill off) in the Poker
Room settings. Removed the old manual add/clear-bot buttons.
v6.6.8
Security log hardening
* The security log now self-caps at ~2MB (rolling to security.log.1) so it
can never grow without bound, and the admin Security Log viewer reads only
the tail of the file instead of loading the whole thing. Keeps the admin
panel fast and memory-safe even on long-running, high-traffic sites.
can never grow without bound, and the admin Security Log viewer reads only
the tail of the file instead of loading the whole thing. Keeps the admin
panel fast and memory-safe even on long-running, high-traffic sites.
v6.6.7
License admin warning cleanup
* Fixed PHP warnings in the master license admin panel when listing older
license records that predate the current key/id fields. Legacy entries
now render cleanly with no notices in the error log.
license records that predate the current key/id fields. Legacy entries
now render cleanly with no notices in the error log.
v6.6.6
Starfield is the new default background
* The Starfield animated background is now ON by default site-wide, on every
edition (LITE included). New sites get it automatically; existing sites
that hadn't set a background switch to it on first load. You can change or
turn it off anytime in Admin -> Themes -> Animated Background.
edition (LITE included). New sites get it automatically; existing sites
that hadn't set a background switch to it on first load. You can change or
turn it off anytime in Admin -> Themes -> Animated Background.
v6.6.5
Per-room animated backgrounds + room topic
* Each room can now set its own animated background, just like room themes.
Pick any of the 15 motion styles (or "None") in the room's settings, with
a live preview. A room's pick overrides the site/per-user background for
everyone while they're in that room.
* New room topic: a short pinned banner at the top of a room (e.g.
"Movie night 8pm!"). Set it on the create form or in room settings.
Pick any of the 15 motion styles (or "None") in the room's settings, with
a live preview. A room's pick overrides the site/per-user background for
everyone while they're in that room.
* New room topic: a short pinned banner at the top of a room (e.g.
"Movie night 8pm!"). Set it on the create form or in room settings.
v6.6.4
Animated backgrounds: 10 new + a live preview
* The animated background pack went from 5 motion styles to 15. New ones:
Sunset fade, Ocean depths, Fireflies, Snowfall, Confetti, Neon grid,
Rolling waves, Plasma swirl, Rising embers, plus a parallax Starfield.
* Rebuilt the older styles so they actually move - real parallax stars,
a full field of rising bubbles, and proper falling Matrix rain.
* Admin and member background pickers now show a LIVE PREVIEW so you can
see the motion before you save it.
Sunset fade, Ocean depths, Fireflies, Snowfall, Confetti, Neon grid,
Rolling waves, Plasma swirl, Rising embers, plus a parallax Starfield.
* Rebuilt the older styles so they actually move - real parallax stars,
a full field of rising bubbles, and proper falling Matrix rain.
* Admin and member background pickers now show a LIVE PREVIEW so you can
see the motion before you save it.
v6.6.3
Poker Room listed for sale as a premium mod
* Poker Room is now a $100 premium mod in the mod store / buy page, and is
registered in the Developer Portal as a live product by Maddog. The
"Buy It All" bundle copy now lists Poker (and the free Puzzles & Music
games), and the "buying separately" savings figure includes it
automatically.
registered in the Developer Portal as a live product by Maddog. The
"Buy It All" bundle copy now lists Poker (and the free Puzzles & Music
games), and the "buying separately" savings figure includes it
automatically.
v6.6.2
Poker AI opponents + My Music in the Community menu
* Poker Room now has AI bot players. Sit down and click "Add bot" to fill
empty seats with computer opponents, so you can play a full hand without
waiting for other people. Bots play a believable tight-aggressive game
(fold trash, value-bet strong hands, occasional bluff), take a natural
couple of seconds to act, are play-money only, and never appear on the
chips-won leaderboard. "Clear bots" removes them.
* Moved "My Music" into the 👥 Community menu (it was a top-level link).
empty seats with computer opponents, so you can play a full hand without
waiting for other people. Bots play a believable tight-aggressive game
(fold trash, value-bet strong hands, occasional bluff), take a natural
couple of seconds to act, are play-money only, and never appear on the
chips-won leaderboard. "Clear bots" removes them.
* Moved "My Music" into the 👥 Community menu (it was a top-level link).
v6.6.1
Animated backgrounds, room fixes, guide & front-page polish
* NEW Animated backgrounds: a motion layer that sits over any theme. Admins
pick one for the whole site and one for the front page (Aurora, Starfield,
Gradient pulse, Floating bubbles, Matrix rain, Neon haze) or paste their
own custom CSS. Members can override with their own pick for their view.
* Stage and Theater cam styles are now correctly disabled in Hangout mode
(floating cams), where they never applied - greyed in the picker and
enforced on the server.
* Removed the per-room word blacklist. It never reliably overrode the
site-wide list, so room chat now uses one clear global word filter.
* Front page expanded with an "Everything's inside" highlights section
(profiles, games, music, capsules, streaks) and a clearer closing sign-up.
* First-Time User Guide now covers Puzzles, Poker, Profile Music, and the
daily login streak milestones.
pick one for the whole site and one for the front page (Aurora, Starfield,
Gradient pulse, Floating bubbles, Matrix rain, Neon haze) or paste their
own custom CSS. Members can override with their own pick for their view.
* Stage and Theater cam styles are now correctly disabled in Hangout mode
(floating cams), where they never applied - greyed in the picker and
enforced on the server.
* Removed the per-room word blacklist. It never reliably overrode the
site-wide list, so room chat now uses one clear global word filter.
* Front page expanded with an "Everything's inside" highlights section
(profiles, games, music, capsules, streaks) and a clearer closing sign-up.
* First-Time User Guide now covers Puzzles, Poker, Profile Music, and the
daily login streak milestones.
v6.6.0
Puzzles arcade, Poker Room, Profile Music, bigger login streaks
* NEW (Puzzles & Word Games - free): a single-player arcade under the Games
menu - daily Word Guess with streaks, Sudoku (Easy/Medium/Hard/Daily),
timed Word Search, and a 90-second Anagram sprint, each with leaderboards.
* NEW (Poker Room - premium, in BUY IT ALL): real multiplayer Texas Hold'em
with up to 6 seats, blinds, side pots, turn timers, a server-authoritative
dealer, and a chips-won leaderboard. PLAY MONEY ONLY - free daily chips,
no real cash - and it respects the casino's self-exclusion.
* NEW (Profile Music - free): a MySpace-style profile player. Members build a
YouTube playlist and pick autoplay, repeat, shuffle, and start-collapsed;
visitors get play/pause, skip, and a track list.
* Daily login streaks now have named milestone tiers (Week Warrior, Fortnight,
Monthly Regular, Diehard, Centurion, Year of Loyalty) with escalating
one-time bonuses, and the streak pill shows the next milestone to chase.
menu - daily Word Guess with streaks, Sudoku (Easy/Medium/Hard/Daily),
timed Word Search, and a 90-second Anagram sprint, each with leaderboards.
* NEW (Poker Room - premium, in BUY IT ALL): real multiplayer Texas Hold'em
with up to 6 seats, blinds, side pots, turn timers, a server-authoritative
dealer, and a chips-won leaderboard. PLAY MONEY ONLY - free daily chips,
no real cash - and it respects the casino's self-exclusion.
* NEW (Profile Music - free): a MySpace-style profile player. Members build a
YouTube playlist and pick autoplay, repeat, shuffle, and start-collapsed;
visitors get play/pause, skip, and a track list.
* Daily login streaks now have named milestone tiers (Week Warrior, Fortnight,
Monthly Regular, Diehard, Centurion, Year of Loyalty) with escalating
one-time bonuses, and the streak pill shows the next milestone to chase.
v6.5.1
Cam live-on-entry recovery + build hardening
* Cam rooms: "see all cams on entry" now reliably shows other broadcasters
LIVE even before you turn your own camera on. Playback triggers when real
frames arrive, and a momentarily black tile auto-recovers by re-initialising
the decoder until the first frame lands.
* Email Mailboxes -> v1.2.0: the admin roster now shows an Auto-login column
(which members have mailbox single sign-on armed) with a per-member
"Revoke login" control to wipe a stored password if an account is at risk.
LIVE even before you turn your own camera on. Playback triggers when real
frames arrive, and a momentarily black tile auto-recovers by re-initialising
the decoder until the first frame lands.
* Email Mailboxes -> v1.2.0: the admin roster now shows an Auto-login column
(which members have mailbox single sign-on armed) with a per-member
"Revoke login" control to wipe a stored password if an account is at risk.
v6.5.0
Live cams on entry, mailbox on the Dev Portal registry
* Receive-only cam connections open the moment you enter a room, so you can
watch without sharing your own camera.
* The built-in Email Mailboxes base mod is registered in the Developer Portal.
watch without sharing your own camera.
* The built-in Email Mailboxes base mod is registered in the Developer Portal.
v6.4.0
Email Mailboxes split into a free standalone mod
* Real name@your-domain inboxes your members read and send on-site: folders,
rich-text compose, signatures, attachments (send + receive), IMAP/SMTP for
any mail app, and cPanel auto-provisioning (with a manual fallback).
* Admin chooses who can claim (everyone, or donors/premium only), the domain,
storage quota, and full attachment rules (on/off, max size, allowed types,
max per message).
* Mailbox single sign-on: a logged-in member is signed into their inbox
automatically (encrypted at rest, ~30-day window, with an off switch).
* Import existing mailboxes from the older donor-program ledger in one click.
* Theme Builder + Donor Program slimmed back to exactly that - theme builder
plus the PayPal donor program.
ABOUT THIS FILE
From v9.7.2 onward this changelog is verified automatically. Every release
scans the shipped source for version markers that have no entry here and
reports anything undocumented before the build is published, so a release
cannot quietly go out without notes.
Entries before that point were written by hand and later audited against the
source; the earliest releases are grouped into ranges rather than listed one by
one. For older history, see your buyer dashboard.
rich-text compose, signatures, attachments (send + receive), IMAP/SMTP for
any mail app, and cPanel auto-provisioning (with a manual fallback).
* Admin chooses who can claim (everyone, or donors/premium only), the domain,
storage quota, and full attachment rules (on/off, max size, allowed types,
max per message).
* Mailbox single sign-on: a logged-in member is signed into their inbox
automatically (encrypted at rest, ~30-day window, with an off switch).
* Import existing mailboxes from the older donor-program ledger in one click.
* Theme Builder + Donor Program slimmed back to exactly that - theme builder
plus the PayPal donor program.
ABOUT THIS FILE
From v9.7.2 onward this changelog is verified automatically. Every release
scans the shipped source for version markers that have no entry here and
reports anything undocumented before the build is published, so a release
cannot quietly go out without notes.
Entries before that point were written by hand and later audited against the
source; the earliest releases are grouped into ranges rather than listed one by
one. For older history, see your buyer dashboard.