]> mop.ddnsfree.com - git repositories - brisk.git/log
brisk.git
8 days agoprint the privileged commands with sudo where sudo is installed
Matteo Nastasi [Wed, 7 Oct 2026 10:04:45 +0000 (10:04 +0000)]
print the privileged commands with sudo where sudo is installed

The commands the "system" action prints, and the ones in the head of the
files it generates, were all "su -c '...'". On debian 13 sudo is installed
and the root account is left without a password, so those are commands
nobody can run.

as_root() now prints "sudo <cmd>" where sudo is on the path and
"su -c '<cmd>'" where it is not, which is still the way on the older
machines this has to keep working on. The one privileged command the action
runs by itself, the copy of the sysv script into /etc/init.d, goes through
sudo on the same condition.

The heads of the generated files also said "run INSTALL.sh again instead",
which stopped being true when they moved to the action: they say
"INSTALL.sh system" now.

Checked in the container: the action prints sudo for the nginx, nftables
and systemd commands, the heads of the three generated files carry the same
form, and as_root with sudo off the path falls back to su -c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8 days agowrite every file of the machine from the "system" action
Matteo Nastasi [Wed, 7 Oct 2026 09:50:11 +0000 (09:50 +0000)]
write every file of the machine from the "system" action

The nginx configuration and the nftables rules were written by an ordinary
install, the systemd unit by "INSTALL.sh system", and the sysv script it
replaces had always been there. Three files that all belong to the machine
and not to the tree of the site, written from two different places.

They now all come from the "system" action, with the same options as the
install: the nginx configuration of the site, the nftables rules of the
daemon machine, and the way the daemon is started. An install writes none
of them. An install is run whenever the code changes; these follow the shape
of the machine, which changes almost never, and writing them every time only
risks overwriting a file somebody has since adapted.

What the action writes depends on the machine, as before: no nginx
configuration with -S apache, on a daemon machine (-m server) or in the
historic -D FALSE mode; no nftables rules unless the daemon is on tcp and -I
gives the address of the front; no daemon on a front, so no unit and no sysv
script there. Only the sysv script is still installed by the action itself,
being the one file that is of no use anywhere else; everything else is
written in Etc and the action ends with the list of what to copy where.

An install now says that the files of the machine come from "system". -h
lists the action. pool_size() reads the installed tree when the tree being
built is not there, which is the case in the action.

Checked in the container: an install of all three roles writes no file of
the machine, and the action writes, for -m full, the three nginx files and
the unit; for -m server, the nftables rules with the pool read from the
installed tree (8090-8099 plus 8100) and the unit; for -m front, the nginx
files alone. The unit passes systemd-analyze verify. nft is not installed
there, so the rules were checked with nft -c in the earlier commits, where
the generator was the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8 days agowrite a systemd unit for the daemon instead of the sysv script
Matteo Nastasi [Wed, 7 Oct 2026 09:32:33 +0000 (09:32 +0000)]
write a systemd unit for the daemon instead of the sysv script

"INSTALL.sh system" wrote a script into /etc/init.d. On debian 13 that
script is never run: the sysv compatibility generator of systemd is gone,
so nothing turns it into a service and nothing starts it at boot.

Where systemd is in charge the action now writes <name>.service in Etc,
beside the nginx and nftables files, and leaves /etc/init.d alone; without
systemd it installs the sysv script exactly as before. The unit is only
written, and the run prints the three commands that install it, as for the
other generated files.

What the unit had to get right, both found by running the daemon:

  - -d. Without it the daemon keeps stdin in its select set, and stdin
    being /dev/null under systemd it reads EOF at once and exits with 22
    (sac-a-push.phh, the branch that returns 22). The sysv script got away
    without it because screen gave the daemon a tty.

  - KillSignal stays SIGTERM. On SIGTERM the daemon serializes the room
    into <legal_path>/brisk-crystal.data and exits 0, and the next start
    reads it back, so a stop and a start keep the games. SIGINT exits with
    1 and saves nothing.

Restart=on-failure, so a stop asked for stays stopped while a crash comes
back up; TimeoutStopSec=15, measured against the 56ms an empty room takes
to serialize; ExecReload sends SIGHUP, which the daemon already treats as
a reload; WorkingDirectory because the daemon reaches its own files by
relative paths. The screen session and the shell loop that respawned the
daemon are both gone, and its output goes to the journal, which the unit
says how to silence.

ngx_put is now gen_put: it is no longer only for nginx files.

Checked in a container with systemd: the unit passes systemd-analyze
verify, the service starts and binds the eleven ports, systemctl reload
reaches the daemon without changing its pid, systemctl stop exits 0 in
50ms leaving brisk-crystal.data behind, the next start logs "ROOM FROM
FILE", and a SIGKILL on the main process brings it back up (NRestarts=1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9 days agoanswer the three refusals of the admin check alike
Matteo Nastasi [Wed, 7 Oct 2026 07:23:57 +0000 (09:23 +0200)]
answer the three refusals of the admin check alike

userauth answered 402 for a session that does not exist, 403 for one that
is not an admin's and 404 for a wrong hash. Whoever reaches the control
channel could tell the three apart and so learn, session by session,
which ones are alive and which belong to an admin.

All three now answer 403 "denied". usermgmt.php, the only client, looks
for 200 and nothing else; test/nonblocking.php does not read the reply.
The reason is written to the daemon log, with the name and the ip of the
user where there is one and never the session. A malformed command still
answers 401: it says nothing about any session.

Checked with the same harness as the previous commit: a good hash answers
200, an unknown session, a non-admin and a wrong hash all answer 403
"denied", each with its own line in the log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9 days agokeep the alarm password and the database credentials out of the reload dump
Matteo Nastasi [Wed, 7 Oct 2026 07:23:24 +0000 (09:23 +0200)]
keep the alarm password and the database credentials out of the reload dump

global_dump(), run after every "reload" of the daemon, printed the globals
it checks to stderr, that is to the daemon log, G_alarm_passwd and
G_dbauth among them: the password of admin.php and the connection string
of the database, password included, in clear in a log file.

The dump is there to show that the globals survived the reload, and for
those two it is enough to know whether they are set: they are now printed
as "(set)" or "(empty)". The other globals are printed as before.

Checked by calling global_dump() with both secrets set and empty: the log
says set or empty and contains neither value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9 days agostop sending the alarm password back on a failed admin check
Matteo Nastasi [Wed, 7 Oct 2026 07:23:08 +0000 (09:23 +0200)]
stop sending the alarm password back on a failed admin check

When the userauth command of the control channel got a wrong hash, the
reply carried the hash received and the string it should have been made
from: G_alarm_passwd, the ip and the session of the admin, in clear, to
whoever had reached the channel. G_alarm_passwd is also the password of
admin.php and the key of the mail verification hashes.

The reply is now a bare "authentication failed". The failure goes to the
daemon log instead, with the name and the ip of the admin and without the
session, which is a credential too. The hashes are compared with
hash_equals rather than !=, which stops at the first differing byte.

Checked by calling direct_command() on a Sac_a_push built without its
constructor and a fake app: the right hash still answers 200, a wrong one
404 with nothing of the secret in the reply, where the previous code
returned it whole.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9 days agoanswer with a static page when the daemon cannot be reached
Matteo Nastasi [Wed, 7 Oct 2026 07:22:27 +0000 (09:22 +0200)]
answer with a static page when the daemon cannot be reached

The daemon urls fell back on error.php for 502, 503 and 504. error.php
needs php-fpm and the database, and when the front sends its php pages to
the daemon machine those are down together with the daemon: the courtesy
page failed exactly when it was needed, and the browser got a bare error.

daemon-down.html carries the same message with no php at all, and reloads
the room after 15 to 25 seconds, as error.php does, so that the players do
not all knock in the same instant when the daemon comes back; a meta
refresh covers browsers without javascript. The banners of error.php are
left out.

nginx serves it with an internal redirect, so the browser still shows the
url it asked for, briskin5/index.php included: every path in the page goes
through <base>, which INSTALL.sh rewrites with the prefix, as it does for
cookiepath. It is installed with -m full and -m front, where nginx runs,
not with -m server. The generated nginx-<name>-daemon.inc and the
brisk-daemon.inc template point error_page at it.

Checked with a -m front installation whose daemon ports are closed: every
daemon url, briskin5 and a POST to index_wr.php included, answers with the
page and keeps the 502 status, and the images it asks for answer 200.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9 days agowrite the nftables rules of the daemon machine, and point at the generated files
Matteo Nastasi [Wed, 7 Oct 2026 07:06:45 +0000 (09:06 +0200)]
write the nftables rules of the daemon machine, and point at the generated files

With the daemon on a machine of its own its ports are open to whoever
reaches them: the pool, where X-Real-Ip is believed and the bans are built
on it; the control channel, which holds the whole daemon up to 3 seconds
per connection; and php-fpm, when it listens on tcp to serve the front,
which runs any php file of that machine for whoever asks.

A "-m server" installation with -U tcp:// now writes in Etc <name>.nft,
<name> from the prefix like the nginx files, to be copied by hand into
/etc/nftables.d/. Its own table, accepting by default and dropping only
the ports of brisk: the pool reachable by the front alone, the control
channel by this machine alone, and lo accepted, because the daemon listens
on its own address and usermgmt.php reaches the control channel from here.
The address of the front comes from the new -I; without it nothing is
written and the run says so. An ipv6 address gives an ip6 saddr rule.

php-fpm is counted as serving the front when one of the pools in
/etc/php/*/fpm/pool.d listens on tcp on an address that is not loopback;
its port then joins the pool in both rules. A unix socket or 127.0.0.1
serves this machine alone and is left out, and the run says that php-fpm
has to be configured first if it is meant to serve the front.

The name of the installation and the size of the pool, until now worked
out inside nginx_conf_gen, are shared with it through inst_name, inst_var
and pool_size.

system/nginx/brisk.conf now says that -S nginx writes the same
configuration already aligned with the installation, and where; it and
WARNING.txt mention -I and the nftables file.

Checked by installing -m server into a scratch tree against fake php-fpm
pools - tcp on the lan, unix socket plus loopback, port alone, ipv6 - and
without -I: every file passes nft -c and loads in an isolated network
namespace. The -m front nginx files come out as before and no .nft.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9 days agolet INSTALL.sh install for nginx alone and write its configuration
Matteo Nastasi [Tue, 6 Oct 2026 16:39:26 +0000 (18:39 +0200)]
let INSTALL.sh install for nginx alone and write its configuration

-S apache|nginx says which http server faces the browser; apache stays the
default, so an installation that does not pass it is the same as before.

With nginx no .htaccess is installed: nginx never reads them. They come
not only from web/ and webtest/ but also from brisk-img and curl-de-sac,
so they are dropped once the whole tree is in place rather than filtered
out of each install. Etc/.htaccess is not written either, and the run says
that the nginx configuration has to deny Etc instead.

That configuration is now written by INSTALL.sh itself, in Etc beside the
brisk configuration, already aligned with -P, -U and the root of the site:

  nginx-<name>.conf         http level: the upgrade map and the upstream,
                            unix sockets or tcp ports from -U, USOCK_POOL_N
                            members read from brisk-spush.phh
  nginx-<name>-server.conf  inside the server {} that terminates the tls:
                            the nine daemon urls, the denies that replace
                            the .htaccess (Etc included when it falls
                            inside the root), php-fpm, the static files,
                            the docroot/ files at the root of the site
  nginx-<name>-daemon.inc   the directives of the nine daemon urls

<name> comes from the prefix, and so do the map variable and the upstream,
so two installations or another site with its own $connection_upgrade can
share one nginx. -F and -B say where php-fpm listens and where the tree is
on that machine, for a front whose php pages run on the daemon host. A
file that changed is kept as .old; one that did not is left alone.

proxy_request_buffering exists from nginx 1.7.11 and older ones refuse the
whole configuration: when the local nginx can be asked its version, the
line is commented out for it. Nothing is written with -m server, where
nginx has no part, nor with -D FALSE, which needs a handover module.

Checked by installing into a scratch tree: front with -U tcp:// and a
remote php-fpm, full with unix sockets, server, and a fake nginx 1.6.2.
The front files pass nginx -t on 1.26.3, and a test nginx running them
answers 200 for the static files and /cookie_law.js, 404 for Obj/, spush/,
.phh, .pho and Etc/, sends the php pages to php-fpm and leaves the .php of
the rest of the site to its own location.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3 weeks agoinstall the server part and the front part separately
Matteo Nastasi [Mon, 21 Sep 2026 07:46:41 +0000 (07:46 +0000)]
install the server part and the front part separately

With the daemon on a machine of its own, the two hosts do not need the
same tree. -m says which one is being installed:

  full    what it has always installed, on one machine
  server  the machine that runs brisk-spush
  front   the machine that runs nginx and php-fpm

The php goes on both, and so do the .js. The frontend serves admin.php,
usermgmt.php and the rest through php-fpm, and those include the same
Obj/ tree - usermgmt.php and mailmgr.php reach spush/brisk-spush.phh for
USOCK_PATH_PFX - so only brisk-spush.php itself, the daemon entry point,
is dropped on the front. The .js are needed daemon side too, because
index.php decides whether to emit the custom.js tag by looking for the
file on disk.

What the daemon never serves is what only a browser asks for, and that is
where the trees part: no stylesheets, no sounds, no brisk-img, nothing
from docroot/. Measured on this installation, "server" installs 85 files
against the 335 of "full", the 223 images being most of the difference.

The frontend has no daemon, so none of the private directories - ftok,
legal, proxy - means anything there: "front" implies -W, which already
meant exactly that. Each mode says at the end what it installed and what
has to be installed on the other machine.

-U now also documents its tcp form, and an unknown mode is refused rather
than silently treated as full.

Checked by installing all three modes into a scratch tree: full 335 files,
server 85 with 443 ftok entries and no image, front 334 with no ftok
entry, no legal directory and no brisk-spush.php.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 weeks agolet the daemon listen on tcp, so it can run on its own machine
Matteo Nastasi [Mon, 21 Sep 2026 07:46:34 +0000 (07:46 +0000)]
let the daemon listen on tcp, so it can run on its own machine

The listening sockets had "unix://" written into Sac_a_push::create, so
the daemon and the frontend had to share a filesystem. USOCK_PATH_PFX can
now also be an address, "tcp://<host>:<port>", and then the pool is the
ports <port>..<port>+USOCK_POOL_N-1 with <port>+USOCK_POOL_N for the
control channel, mirroring the unix names one for one. A prefix without a
scheme keeps meaning exactly what it meant, so nothing changes for an
installation on a single machine.

spu_listen_url() and spu_admin_url() in spush/brisk-spush.phh do the
translation, and they live there because usermgmt.php includes that file
for USOCK_PATH_PFX and reaches the control channel through it. The
unlink of stale socket files is now skipped when there is no file to
unlink, and a failed bind says which address it failed on instead of
returning FALSE in silence.

What made this small is that the frontend and the daemon share no state
but the database. The System V shared memory - challenges, hardbans, the
room - is only ever touched from the nine endpoints the daemon serves;
usermgmt.php, mailmgr.php, error.php and the briskin5 statistics pages
include auth.phh for its database classes and never call
Challenges::load_data(). Under the old descriptor handover that bridge
did exist; moving to direct http had already dissolved it.

Two things the address form changes, both written out in brisk-spush.phh
and in the nginx template: the daemon still believes X-Real-Ip, which is
what the bans and the blacklist are built on, so a port anybody can reach
is a way around every ban; and it still speaks plain http, so the session
cookies cross the network in clear.

Checked end to end: with -U tcp://127.0.0.1:8090 the daemon listens on
8090..8100, answers on several ports of the pool, and 60 players through
nginx over that upstream complete with no error and clean stream
integrity, gzip included. Back on a unix prefix everything is as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks agopoint the B.U.G. page at a banner that exists php84-port-en
Matteo Nastasi [Sun, 13 Sep 2026 15:22:41 +0000 (15:22 +0000)]
point the B.U.G. page at a banner that exists

bug.php asked for img/bug_bannerbig93.png, which is not in brisk-img: the
page came up with a broken image at the top. The repository carries
bug_bannerbig80.png (468x60) along with the mid and small variants, so
the 93 was renamed or dropped at some point without the page following.

Every image referenced from the js, php and css under web/ now answers
200, except img/probrisco.jpg, which sits inside a commented out rule in
dnd.css and is asked for by nobody.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fseposbqj9mBbQXoT8KnBj

4 weeks agosay so when installing a site without its images
Matteo Nastasi [Sun, 13 Sep 2026 15:16:42 +0000 (15:16 +0000)]
say so when installing a site without its images

brisk-img carries every image of the site: the cards, the table, the
icons. INSTALL.sh picks it up from beside this repository, and when it is
not there it used to skip it without a word - so a run that had quietly
dropped every image looked exactly like a good one, and the site came up
with each img answering 404.

That is how this went unnoticed: the bench was moved into the container
with only the brisk repository mounted, brisk-img was left outside, and
several runs of INSTALL.sh rebuilt the tree without images while
reporting success.

It now warns, and says what to do about it. curl-de-sac gets a milder
note on the same spot: the code that uses it is guarded by
defined('CURL_DE_SAC_VERS'), so without it the feature is simply absent
rather than broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fseposbqj9mBbQXoT8KnBj

4 weeks agoadd a custom.js sample file and add custom.js to .gitignore file
Matteo Nastasi [Sun, 13 Sep 2026 15:14:54 +0000 (17:14 +0200)]
add a custom.js sample file and add custom.js to .gitignore file

4 weeks agorun the bench from the repository instead of a copy
Matteo Nastasi [Sun, 13 Sep 2026 15:08:54 +0000 (15:08 +0000)]
run the bench from the repository instead of a copy

The bench used to be a copy kept in /root/load, run as root, driven from
outside the container. The paths were written into the scripts and the
copy went stale more than once. They now run straight out of the working
tree, unprivileged, and read common.sh for the three settings they share.

BRISK_WORK holds the working files, /var/tmp/brisk-load by default. Not
/tmp, which is a tmpfs here and gets wiped by a restart of the container
in the middle of a session - that is why the scripts had been moved to
/root/load to begin with - and not the script directory either, or
auth.txt and the captured streams would be left lying in the git tree.
BRISK_LEGAL is where the daemon keeps its log and its pid file, and SUDO
covers the few things that still need root: restarting the service, and
reading the open descriptors of a daemon that runs as another user.

brisk_step.sh no longer starts the daemon by hand. It went through a raw
screen invocation of its own, which is precisely how orphaned instances
used to pile up: the newcomer unlinks the socket files and binds its own
while the first one stays alive on its own shared memory. It restarts the
service now. Its two log counts also become deltas: both logs are
cumulative, so the totals said nothing about the step that just ran.

The trap that reaps the background loops changes shape. It was
"trap 'kill 0'", which signals the whole process group - almost always
wider than the script. It killed the script itself on the way out, so the
exit status was always a death by signal, and it reached whatever else
shared the group: the caller, or the other half of a pipeline, so that
"brisk_step.sh | tail" printed nothing at all. Running everything through
"incus exec" gave each script a session of its own and hid all of it.
bench_reap walks our own descendants instead, which reaches the loops
inside "( ... ) &" and the curls under them and nothing else.

Checked from the working tree: brisk_step.sh completes through a pipe
with its output intact and exit status 0, the sampler reads the daemon
descriptors through sudo, game.sh forms the table, stop.sh reports a
clean machine, and INSTALL.sh installs and restarts from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fseposbqj9mBbQXoT8KnBj

4 weeks agoadd the probe for the short write to the bench
Matteo Nastasi [Sun, 13 Sep 2026 13:55:56 +0000 (15:55 +0200)]
add the probe for the short write to the bench

partial.py was written while chasing the truncated chunk but never
committed: it stayed in the scratch directory inside the container, so
the one tool that exercises the resumption after a short write against
the running daemon was not in the tree.

It opens a daemon socket directly, the way nginx does in direct http
mode, with an SO_RCVBUF small enough that the daemon cannot place the
whole first payload, waits without reading, then walks the chunked
framing and pairs the @BEGIN@/@END@ blocks.

It does not trigger the defect on its own: the room bootstrap is far
below the socket buffer, so the daemon never writes short. It is here to
check the framing against the real daemon, next to the socket pair
reproduction that did trigger it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agostop the daemon instances from piling up
Matteo Nastasi [Sun, 13 Sep 2026 13:26:29 +0000 (15:26 +0200)]
stop the daemon instances from piling up

Two defects let a second daemon start beside a running one, and once
that happened the init script could not stop either of them again.

pid_save() wrote its pid over whatever was in brisk.pid, and pid_remove()
deleted the file without looking at whose pid was in it. So an instance
exiting after another one had taken the file over left the survivor
unrecorded, and from then on "stop" found no pid to kill: every restart
added an orphan instead of replacing it. The orphan was not idle - a
starting daemon unlinks the socket files and binds its own, so it takes
every new connection while the old one stays alive on its own shared
memory, and nothing says so.

brisk.pid is now opened once and held under an exclusive non blocking
lock for the whole life of the daemon. The kernel drops the lock when the
process dies, however it dies, so neither a stale file left by a crash
nor two instances starting in the same instant can get through - and
checking the recorded pid for liveness could not have covered the second
case. A daemon that cannot take the lock says who holds it and exits 3.
pid_remove() only unlinks the file while it still holds the lock.

In the init script the pipe into grep was written "\|", so it never was
a pipe: the daemon was run with "|", "grep" and "IN LOOP" as three extra
arguments, and the loop that restarts it watched the wrong exit status.
Harmless in itself - Sac_a_push::create only looks for -d and --daemon -
but the loop never worked as intended and the junk showed up in ps.

"stop" now also quits our screen sessions. Killing the process alone was
never enough, because each session carries the loop that respawns it; and
a session left behind by a start that was refused would sit there and
grab the daemon at the next stop. The loop also sleeps a second between
attempts, so a daemon that cannot start does not spin.

Checked in the container: stop brings a deliberately dirty state (two
daemons, four screens) back to nothing; three restarts in a row leave
exactly one daemon with a pid file that matches it; a second start is
refused naming the holder; a kill -9 leaves the file behind and the next
start takes it over anyway. 100 players with gzip afterwards: no errors,
stream integrity clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agocheck the compressed stream under load
Matteo Nastasi [Sun, 13 Sep 2026 12:37:55 +0000 (14:37 +0200)]
check the compressed stream under load

brisk_load.py read the stream as raw bytes and counted the chat lines in
them, which only works while nothing is compressed. With --encoding it
now asks for gzip or deflate, walks the chunked framing by hand and
inflates what comes out of it, so the bench covers the path that the
truncated chunk and the content coding fixes just touched.

Three counters are reported, and anything but zero means the stream is
coming apart: chunks that do not add up (short, or not closed by their
CRLF), compressed streams that stop inflating, and answers whose
Content-Encoding is not the coding that was asked for.

Seven runs of 150 registered players, plain, gzip and deflate, 60 and 90
seconds: all three counters stayed at zero, together with the write,
login and stream error counts. The coding is worth about a third of the
bytes on the wire (31%, repeatable across runs), and the chunked framing
costs 7.5% on top of the content when nothing is compressed.

The README says not to compare codings on the messages delivered: that
number swings by a factor of two between two runs of the same coding,
because the readers drain at different rates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agofix the content codings announced on the stream
Matteo Nastasi [Sun, 13 Sep 2026 11:31:15 +0000 (13:31 +0200)]
fix the content codings announced on the stream

Three inconsistencies between what the headers promised and what went
out on the socket:

- headers_render() announced "Content-Encoding: chunked" on every
  streaming response the client had not asked to compress. chunked is a
  transfer coding, not a content coding. Apache rewrote the response and
  it went unnoticed; nginx forwards the header as it stands, and a
  browser that meets a content coding it does not know refuses the whole
  body. Transfer-Encoding: chunked, right below, already says it.

- the deflate stream was built without stating the window, and the
  filter then emits raw deflate, while Content-Encoding: deflate
  promises the zlib wrapper of RFC 1950. Whoever asked for deflate got
  bytes that could not be inflated as announced.

- get_encoding() compared the tokens of Accept-Encoding as they came out
  of explode(), with their leading space and their quality attached.
  "gzip, deflate, br" only ever matched its first token, and a coding
  refused with q=0 was taken as accepted.

Checked on the daemon socket, reading the raw stream and inflating it:
gzip and deflate both come out whole across several chunks, every chunk
self contained, the blocks paired and nothing duplicated; with no
Accept-Encoding no Content-Encoding is sent at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agofix truncated chunk left behind by a short write
Matteo Nastasi [Sun, 13 Sep 2026 11:31:09 +0000 (13:31 +0200)]
fix truncated chunk left behind by a short write

When fwrite() could not place the whole response on the socket, the part
still to be sent was sliced out of the wrong string: $wret is an offset
into $response, which carries the http headers and the chunk framing
ahead of the content, but the slice was taken from $content. The client
was then served a chunk shorter than its declared length and without its
terminator, followed by bytes with no framing at all: the javascript
parser lost the @BEGIN@/@END@ boundaries and never found them again,
which is the comet stream falling apart. sac-a-push.phh already did this
right; brisk.phh and briskin5.phh did not.

Reproduced on a socket pair whose send buffer is smaller than the
payload: with the old slice the chunk declared 324012 bytes and only
323908 followed, with the new one the framing stays consistent and the
content comes out whole. It takes a response above the ~200 KB of the
socket buffer to show up, which is why a full room (240 players, ~18 KB
of bootstrap) never triggered it.

Two more defects of the same family:

- compress_chunk() re-wrote the whole input after a short write into the
  deflate stream, instead of the part that was still missing, so the
  client inflated a chunk carrying duplicated content.

- chunked_fini() returned "0\r\n" without the CRLF that closes the
  trailer section, that is an unterminated last chunk. It has no callers
  today (stream_close() does the work), but it was the same mistake
  waiting to be made again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoper-user graphical customisations handled on the client side
Matteo Nastasi [Sun, 13 Sep 2026 11:06:05 +0000 (13:06 +0200)]
per-user graphical customisations handled on the client side

4 weeks agotest/load: the test bench used for the checks
Matteo Nastasi [Sun, 13 Sep 2026 10:41:31 +0000 (12:41 +0200)]
test/load: the test bench used for the checks

The tools the port was verified with: load generator, resource sampler, driver
for a five player game, and the probes for the corner cases of the transport
(duplicate session, expired session, full queue, socket backlog). The README
says what is needed first - the test users in the database, with the note
about the guar_code constraint that makes the obvious insert fail - and what
each tool is for.

They are not part of the application: they are laboratory stuff, and the
README says so.

The generator is a single asyncio process because it has to be cheap: it runs
on the same machine as the daemon, and a heavy generator would distort the
very measurement it is taking.

Every script that starts background loops carries a trap that kills its own
process group, and there is a stop.sh to run at the end of a session. This is
not pedantry: without it the loops outlive their parent, and in this session
eighty processes stayed alive for two days, with the machine load at 19 and
the disk full of capture files. stop.sh also avoids "pkill -f", which kills
the shell that runs it when the pattern shows up in its own command line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe init script could not start the daemon
Matteo Nastasi [Sun, 13 Sep 2026 10:39:48 +0000 (12:39 +0200)]
the init script could not start the daemon

"su - www-data" answers "This account is currently not available": the system
user that runs the daemon has /usr/sbin/nologin as its shell, and that is the
case for www-data on debian. The init script used it in both start branches,
start and devstart, so the service did not start at all.

"-s /bin/bash" is passed to the two su. It is the bare minimum: giving the
system user a real shell would be the wrong fix.

Checked in the debian 13 container: installed with "INSTALL.sh system",
enabled with systemctl (systemd accepts LSB scripts through
systemd-sysv-install), and after a restart of the container the daemon, nginx
and postgresql come back up by themselves, with the site answering on 80 and
443.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agowhoever is displaced by another access goes back to the login
Matteo Nastasi [Sun, 13 Sep 2026 10:39:37 +0000 (12:39 +0200)]
whoever is displaced by another access goes back to the login

The farewell added a moment ago stopped the stream and showed a notice, the
same in every case. But the cases are two, and they want two different
answers.

If the two windows share the SAME session, it cannot be sent to the login:
the session cookie is shared between the tabs of the same browser, so
index.php would let it straight back in and the bouncing would start again.
It was checked that deleting the cookie from the displaced window would not
be enough either: the daemon reads the session ONLY from there (a stream with
sess in the url but no cookie is refused), so deleting it would disarm the
winning window as well, since the cookie is shared. For this case the notice
without a return to the login stays.

If instead the old stream belonged to another session - the "ghost swap" of
add_user(), that is a new access with the same name from another browser -
its session is orphaned by now and it must go back to the login: there
ghost_sess shows it "La tua sessione e' stata assegnata ad un altro browser"
and it does not come back in, because that session is no longer valid.

To tell them apart, the session the stream was opened with is recorded in
rd_sess, and when it is replaced that is compared with the one of the
newcomer.

Checked in both cases: same session -> xstm.stop() and the notice, no return
to the login; different session -> xstm.stop() and a return to index.php,
where the message really shows up. A complete game confirms it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe displaced stream was closed without a word
Matteo Nastasi [Sun, 13 Sep 2026 10:39:10 +0000 (12:39 +0200)]
the displaced stream was closed without a word

When a new read stream arrives for a session that already has one, the daemon
replaces the old with the new: that is the intended behaviour, it is what
lets you carry on from where you were after a browser restart.

The displaced client, though, was told nothing: its socket was simply closed.
It reopened it, displacing the new one in turn, which reopened in turn: two
windows open on the same session bounced each other forever, each with a red
indicator and no explanation.

Now the replaced stream is dismissed: it stops the streaming and shows a
notice. It is not sent back to the login, because when the two windows share
the same session the entry page would let it straight back in, starting the
bouncing again.

The message travels over the transport of the OLD connection, which may
differ from the one of the new, and is followed by an orderly close
(stream_bye in user.phh). In the normal case - the same client reopening
after having given the connection up for lost - it ends up on a socket nobody
reads any more and does no harm.

Checked with two streams on the same session, in both combinations: the first
receives xstm.stop() and the notice, as text over xhr and framed (0x81) over
websocket, and is not sent back to the login. A complete game confirms the
normal path is intact.

NOTE: the case of a login by a user already present was handled before, by
the "ghost" mechanism of add_user(): the new access inherits the old user,
seat at the table included, and the displaced session is recorded in
ghost_sess with reason ANOT, which index.php turns into "La tua sessione e'
stata assegnata ad un altro browser". What was missing is precisely the piece
added here: if the displaced client was reading a stream, it never reached
index.php and never saw that message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoinvalid session over websocket: the client never got to know
Matteo Nastasi [Sun, 13 Sep 2026 10:38:49 +0000 (12:38 +0200)]
invalid session over websocket: the client never got to know

Whoever connected with an expired session received an ordinary http response
with an html page inside. To the browser WebSocket API that is just a failed
upgrade, whose content it cannot read: the client never learned it had to go
back to the login, and retried forever with a red indicator. On xhr, instead,
the same case had always worked.

The root is in Transport::gettype(), which did not know "websocketsec": the
encrypted variant uses the same Transport_websocket class, told apart only in
the constructor (create() does treat them together). Missing from the list, it
fell back on Transport_iframe, that is on an html page.

Three places that contributed to the same symptom were fixed:

 - Transport::gettype() recognises websocketsec;
 - stream_fini() completes the handshake when the transport is websocket, so
   that the exit command arrives as a real message over an established
   connection, instead of as the body of a response nobody will read;
 - Transport_websocket::fini() frames the message, which used to go out raw.

On top of that, in the table path the farewell was built with $transp_type,
which is the transport guessed from the User-Agent and not the requested one:
for a websocket client it was "xhr".

Checked: with a session that does not exist the answer is now 101 Switching
Protocols with a text frame (0x81) carrying the command to go back to the
login and the close frame right after, both for websocket and for
websocketsec. With a valid session the stream stays open and receives its
frames as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe watchdog reloaded the page even while data was arriving
Matteo Nastasi [Sun, 13 Sep 2026 10:38:27 +0000 (12:38 +0200)]
the watchdog reloaded the page even while data was arriving

If for 16 seconds (keepalives_eq_max x watchdog_checktm x watchdog_timeout)
the keepalive counter does not move forward, the client declares the
connection dead and reloads the page, splash included.

But that counter only moves forward when the client manages to CONSUME the
incoming data. If the browser is busy - redrawing the list of a crowded room,
say - the data arrives regularly and sits there waiting to be parsed: the
counter stays put and the watchdog reloads a perfectly healthy connection,
throwing away the work done.

Now, before declaring it dead, it looks at whether there is still something
to consume: bytes received and not parsed yet, commands already extracted and
not executed yet, or slow actions in progress (st_loc < st_loc_new, which is
the client itself saying it is busy). In those cases the connection is alive
and it gets another round instead of a reload.

The timeout stays at 16 seconds: what changes is the criterion, not the
patience. A connection that really is gone receives nothing any more, so the
three indicators are at zero and the reload happens as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agomessage queue per user tripled, and the ceiling that holds it
Matteo Nastasi [Sun, 13 Sep 2026 10:38:08 +0000 (12:38 +0200)]
message queue per user tripled, and the ceiling that holds it

If the client falls behind by more than COMM_N messages the history is lost:
it is sent back to the initial page, splash included. That happens when it
reconnects after a burst, for instance while the garbage collector removes
dozens of disconnected users at once and every removal produces a room
update.

Eighteen messages are few, but they could not be raised on their own: every
queued update carries the complete list of the people present
(standup_content), which with a full room is about 3 KB, and eighteen of
those were already 61 KB against the 65536 of SHM_DIMS_U_MAX. Raising COMM_N
without raising the ceiling would have made shm_put_var fail. The segments
grow by SHM_DIMS_U_DLT at a time, so whoever does not fill the queue pays
nothing.

WARNING: changing COMM_N means the queue indexes (step % COMM_N) no longer
match those the users were saved with. The persisted state has to be wiped:
without that, the tables stay in "sitreser" and never form again. Verified in
the field, in both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe comet stream was closed without the final chunk
Matteo Nastasi [Sun, 13 Sep 2026 10:37:55 +0000 (12:37 +0200)]
the comet stream was closed without the final chunk

Streaming responses declare Transfer-Encoding: chunked and the data goes out
framed by chunked_content(), but on close the zero length chunk that
terminates the response was never sent.

With apache it went unnoticed: the daemon owned the client socket and simply
closed it. nginx instead parses the response, and every stream that renews
itself (RD_ENDTIME_DELTA, 240 seconds) looked truncated to it: "upstream
prematurely closed connection while reading upstream", one error line per
connected player per cycle.

The final chunk is added in User::stream_close(), which is already the place
where the daemon writes its last bytes before closing, and only for the
transports that really are chunked: websockets are not, and keep sending
their own close frame.

Measured: with 150 players the renewal of the streams produced 150 errors;
after the fix, 30 players and 30 stream reopenings give zero errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe chat redrew the box one line at a time
Matteo Nastasi [Sun, 13 Sep 2026 10:37:31 +0000 (12:37 +0200)]
the chat redrew the box one line at a time

Once the CHATT_MAXLINES ceiling was reached, every incoming message emptied
the box and then filled it line by line, assigning innerHTML on each turn.
Each += on innerHTML forces the browser to reserialise the content, reparse
it and rebuild the subtree: 41 rebuilds of the DOM per message, with a cost
that grows with the square of the number of lines.

Now the shift happens on the array and the box is rewritten once.

It came out of the load test: with 150 players chatting (15 messages a second
broadcast to everybody) a real browser saturated twelve cores and died, while
the daemon serving that traffic sat at 6% of one core. The bottleneck was the
client, not the server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agocustom.js is optional, and the page no longer demands it
Matteo Nastasi [Sun, 13 Sep 2026 10:37:13 +0000 (12:37 +0200)]
custom.js is optional, and the page no longer demands it

index.php always loaded it, but custom.js holds the customisations of the
single site and may not be distributed: whoever installs from scratch got a
404 on every page load, on both branches of the layout.

The tag is now emitted only if the file is really there. The hook it uses
(custom_bedge) was already guarded on the javascript side, so without the
file the room behaves exactly as it always has.

Same approach as cookie_law.* and brisk_donate.txt: what belongs to a single
installation must not break the others.

Checked in the container: with the file the page references it and downloads
it (200), without the file the page is served complete, does not name it and
nginx logs no 404. Both occurrences were tried, the one on the entry page and
the one in the room.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agosql/builder.sh: sudo instead of su
Matteo Nastasi [Sun, 13 Sep 2026 10:37:07 +0000 (12:37 +0200)]
sql/builder.sh: sudo instead of su

"su root -c" asks for the root password, which on a freshly installed debian
does not even exist: the first user administers with sudo. The two commands
were also nested (su root calling su postgres) with three levels of quoting
to get through, and the variables ended up in the string unprotected.

Now it goes straight to "sudo -u postgres" and the arguments are quoted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe listen queue of the daemon sockets was the default one
Matteo Nastasi [Sun, 13 Sep 2026 10:37:01 +0000 (12:37 +0200)]
the listen queue of the daemon sockets was the default one

With the php default backlog a single socket of the pool queues 111
connections and then refuses: measured with a burst of 300 simultaneous
connects. The frontend gets EAGAIN on the connect and has to fall back on
another socket of the pool, or return an error.

It showed up during the load test: with 300 entries close together nginx
logged "connect() to unix:...brisk0.sock failed (11: Resource temporarily
unavailable)". With the backlog at 511 the same burst goes through entirely
and the error does not come back.

Until now the pool of ten sockets masked the problem, spreading the
connections over ten short queues instead of one long queue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe donation button was not distributed anywhere
Matteo Nastasi [Sun, 13 Sep 2026 10:36:37 +0000 (12:36 +0200)]
the donation button was not distributed anywhere

index.php reads it from FTOK_PATH/brisk_donate.txt, but that file was not in
the repository: whoever installed from scratch had no button, and the daemon
wrote a file_get_contents warning on every page load.

The html fragment now lives in data/, next to the other data files of the
installation, and INSTALL.sh copies it into FTOK_PATH only if it is not there
already, so as not to overwrite the one of the installation.

The file stays optional: index.php checks that it exists before reading it,
so whoever does not want the button does not get a dirty log (the code
already handled the case, but only after the warning had been written).

Checked in the container with a full installation, no longer just -W: the
file lands in FTOK_PATH, the page shows the form, the log stays clean;
without the file the page still loads with no form and no warning; and a
second installation does not touch the file already there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agothe configuration template asked for a document that did not exist
Matteo Nastasi [Sun, 13 Sep 2026 10:36:20 +0000 (12:36 +0200)]
the configuration template asked for a document that did not exist

$G_tos_vers stayed at "1.2" while the documents in Obj/doc/ were renamed
twice, in 2014 and in 2015, up to 1.4: the last time the two values were
aligned was in 2013.

The effect on a fresh installation: file_get_contents does not find the file,
writes a warning in the log and returns FALSE, and the user is asked to accept
an empty document anyway.

Checked in the container with 1.2 and with 1.4, everything else being equal:
with 1.2 the dialog arrives with no text and the warning is in the log, with
1.4 the text of the document arrives and the log is clean.

The switchover dates ($G_tos_dthard and $G_tos_dtsoft, november 2013) are left
as they are: they are in the past, so on a fresh installation the current
document has to be accepted right away, which is the intended behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoINSTALL.sh: the -a option had no effect at all
Matteo Nastasi [Sun, 13 Sep 2026 10:35:51 +0000 (12:35 +0200)]
INSTALL.sh: the -a option had no effect at all

The sed looked for the BRISK_AUTH_CONF define in Obj/auth.phh, where it has
never been: it lives in Obj/dbase_file.phh. sed did not find the file, wrote
the error in the middle of the other installation lines, and the installation
carried on, so -a was ignored without anybody noticing.

The define is now looked for where it actually is, and if it is not found the
installation says so instead of keeping quiet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agophp8: the result of a call assigned by reference
Matteo Nastasi [Sun, 13 Sep 2026 10:35:45 +0000 (12:35 +0200)]
php8: the result of a call assigned by reference

"Only variables should be assigned by reference": the return value of a
function is not a variable, and none of the three methods called declares
that it returns a reference. With objects the & has been useless since php 5,
and every other place that calls get_user() already assigns without it.

The Notice came out on every card played (briskin5's index_wr.php) and was
invisible until the daemon log was cleaned up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agodirect http mode becomes the default
Matteo Nastasi [Sun, 13 Sep 2026 10:35:02 +0000 (12:35 +0200)]
direct http mode becomes the default

The daemon no longer receives the client descriptor handed over by the
frontend: nginx opens an ordinary http connection to it on one of the unix
sockets of the pool. Neither the php ancillary extension, nor
mod_proxy_fdpass2 for apache, nor ngx_http_fdpass_module for nginx, nor kTLS
are needed any more: the debian nginx package is enough. In exchange nginx
stays in the middle for the whole life of the connection, which in a comet
application is long.

The historic mode is one INSTALL.sh -D FALSE away.

The sample configuration was added under system/nginx/, and WARNING.txt was
rewritten: it only documented the old apache setup.

Two log traces had to be fixed before the direct mode could be used, because
they fired once per request instead of occasionally:

 - "User associated with ID: N not found" was printed for every socket with
   no user attached, that is for the head of every request. The message now
   comes out only if the socket is in none of the known lists.

 - "PP_REM" traced the removal of a pending page, which now happens on every
   request: it moves under debug > 1.

Checked in the debian 13 container with the packaged nginx 1.26.3 (no added
module): static pages, the .htaccess protections rewritten in the
configuration, the login of five certified users, comet streaming, POST, and
three complete five player games over https recorded on postgresql. The
daemon log for a whole hand went from thousands of lines to about forty.

Behaviour under load and the reuse of connections between nginx and the
daemon remain to be checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoINSTALL.sh: the root of the site is no longer asked of apache
Matteo Nastasi [Sun, 13 Sep 2026 10:33:53 +0000 (12:33 +0200)]
INSTALL.sh: the root of the site is no longer asked of apache

    document_root="$(grep DocumentRoot "${apache_conf}" | ... | awk '{ print $2 }')"

Three defects in one line: it tied INSTALL.sh to apache (nginx uses "root",
not "DocumentRoot"), it took the first match in any VirtualHost without
knowing which site was the right one, and it did not cope with quotes around
the path.

The value, though, can already be deduced from the parameters: web_path ends
with prefix_path, so removing the latter from the former leaves the root.

    web_path    = /home/brisk/web/brisk
    prefix_path = /brisk/
                  -------------------
    root        = /home/brisk/web

No server to ask, and it works with multi segment prefixes too
(/var/www/html/games/brisk with -P /games/brisk/ gives /var/www/html).

The -R option was added to force an explicit value, and the old grep over the
file named by -A was kept as a last resort; it now recognises both
DocumentRoot and root and strips quotes and semicolons. If no route produces
a value, INSTALL.sh stops with a clear message instead of going on with an
empty string (which ended up producing requires of
"/Etc/brisk_spu.conf.pho").

The derived value is now printed among the parameters, like the others.

Checked in the container by running INSTALL.sh WITHOUT -A: it derives
/home/brisk/web, writes it into $DOCUMENT_ROOT inside spush/*.ph* and
donometer.php, and installs the files from docroot/ there. The site answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoerror.php and doc_download.php: $DOCUMENT_ROOT was never set
Matteo Nastasi [Sun, 13 Sep 2026 10:33:39 +0000 (12:33 +0200)]
error.php and doc_download.php: $DOCUMENT_ROOT was never set

Both scripts include Obj/brisk.phh, which on line 94 does

    require_once("$DOCUMENT_ROOT/Etc/".BRISK_CONF);

but neither of them set $DOCUMENT_ROOT. The path collapsed to
"/Etc/brisk_spu.conf.pho", the require failed and the page answered 500.

This is not a consequence of the port: the git history shows that
doc_download.php never had that line in two commits, and error.php does not
mention it at all. INSTALL.sh substitutes $DOCUMENT_ROOT only in spush/*.ph*
and donometer.php (line 445), not in these two.

NOTE: in the working copy doc_download.php carried a local fix that was never
committed, with the path written by hand
($DOCUMENT_ROOT="/home/nastasi/web"). Since INSTALL.sh distributes from the
working copy and not from git, that is probably what runs in production: a
fix that existed on one disk only and would have disappeared at the first
clone onto a new machine. This commit replaces it with the portable form.

Used the scheme already present in usermgmt.php, mailmgr.php,
briskin5/statadm.php and the others, which derive the value from $_SERVER: it
works both with mod_php and with php-fpm and does not depend on a hardcoded
path. $G_base = "" was added to doc_download.php too, which brisk.phh:95 needs
and which was missing.

Found by handing the pages not given to the daemon over to php-fpm, which
under apache were served by mod_php: both answered 500. After the fix: 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agodocroot/: the files that belong in the root of the site
Matteo Nastasi [Sun, 13 Sep 2026 10:33:22 +0000 (12:33 +0200)]
docroot/: the files that belong in the root of the site

cookie_law.css and cookie_law.js are referenced by index.php with a leading
slash ("/cookie_law.js", lines 1047, 1048, 1221, 1222), so the browser asks
the DocumentRoot for them and not the subdirectory of the application.
INSTALL.sh installs everything inside $web_path, that is in /brisk/: putting
them under web/ would still land them in the wrong place.

They only lived as loose, untracked files in the working copy, and on a new
machine they would simply have been missing. nginx reported them as 404 on
every page load.

The docroot/ directory was created, its name declaring its destination, and
INSTALL.sh was taught to copy its content into $document_root: the same value
it already writes into $DOCUMENT_ROOT, so no second source of truth is
introduced.

Checked by deleting the two files from the container and running INSTALL.sh
again: it puts them back by itself.

NOTE: it depends on the "grep DocumentRoot" over the apache configuration
file (line 444), which remains the coupling to apache already pointed out.
Dropping apache means replacing it, and at that point it serves both uses.

custom.js stays out: it is untracked too, but referenced without a leading
slash, so it lives inside /brisk/ and it is enough to add it under web/ for
INSTALL.sh to distribute it with no further change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoalternative mode: nginx speaks http directly with the daemon
Matteo Nastasi [Sun, 13 Sep 2026 10:33:07 +0000 (12:33 +0200)]
alternative mode: nginx speaks http directly with the daemon

Prototype of an architecture that removes the descriptor handover. Turned on
with SPU_HTTP_DIRECT in brisk-spush.phh; the default stays FALSE, that is the
historic behaviour, and the two modes live side by side.

Today the descriptor the daemon receives is the one of the nginx->apache
connection, already in clear because nginx stripped the TLS one hop earlier.
With this mode nginx opens an ordinary http connection on the unix socket and
the daemon uses it directly.

What it takes away:
 - php-ancillary, the C extension
 - mod_proxy_fdpass2, the apache module
 - ngx_http_fdpass_module, the nginx module
 - apache as an intermediate layer
 - the kTLS requirement, which would be needed if nginx were to hand over the
   browser descriptor (encrypted) instead of the cleartext one towards apache

That is three pieces of bespoke C, each of which needed a port in this very
migration, plus a kernel requirement. The daemon becomes an ordinary http
server behind a reverse proxy.

The code:
 - spu_head_end() and spu_head_to_info() repackage the request read from the
   network in the same format the control channel produced ("The-Request:" in
   front of the request line), so that spu_process_info() does not know where
   the data comes from
 - the head of the request is NOT read by blocking: there is a single event
   loop for every player. The connection is registered in the
   PENDINGPAGE_WAITHEAD state among the watched sockets and completed
   incrementally, reusing the machinery already in place for the bodies of
   partial POSTs; if the body is still missing, it moves on to
   pendpage_try_addwait seamlessly
 - trim() on the header value: it was missing, and real http writes
   "Name: value" with a space, which would have ended up inside the value,
   breaking the cookies and the comparisons on Upgrade

Checked in the container with nginx 1.26.3 and php 8.4, without apache: the
page, an authenticated login, comet streaming, POST, and a complete game over
HTTPS with five players, the auction, 40 cards and the score saved on
postgresql. Secure websockets work (101 Switching Protocols and frames from
the daemon), confirmed from chrome too. Zero errors in the daemon.

Not verified yet: behaviour under load, and the reuse of connections between
nginx and the daemon (in the tests keepalive towards the upstream is off).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoheaders_render: three duplicated headers in every response
Matteo Nastasi [Sun, 13 Sep 2026 10:32:24 +0000 (12:32 +0200)]
headers_render: three duplicated headers in every response

Http header names are case insensitive, php array keys are not. The
transports set "Content-type" with a lowercase t (transports.phh:568 and
:618, index.php:1025 and :1199) while headers_render() checked for
"Content-Type": the check never saw it and added the default anyway.

Same dynamic for Expires and Cache-Control, which force_no_cache() sets and
headers_render added again without checking at all.

Every response therefore went out with:
  Content-Type: text/html         +  Content-type: text/html; charset="utf-8"
  Expires: -1                     +  Expires: Mon, 26 Jul 1997 05:00:00 GMT
  Cache-Control: no-cache         +  Cache-Control: no-cache, must-revalidate

With apache this went unnoticed: the daemon wrote the bytes straight to the
client and the browser applied the last value. Behind a reverse proxy the
response is parsed instead, the first value wins and the second is dropped:
the charset was lost, and in transports.phh:568 an application/xml was
replaced by text/html.

Fixed at the root rather than in the five calling places: headers_render
builds a map of the keys normalised to lowercase and uses it for every
check, so the defect does not come back if somebody writes "Content-type"
again tomorrow.

Found by putting nginx in front of the daemon: it reported "upstream sent
duplicate header line" 123 times. After the fix: zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoshort <? tags turned into <?php: the page was unusable in the browser
Matteo Nastasi [Sun, 13 Sep 2026 10:32:06 +0000 (12:32 +0200)]
short <? tags turned into <?php: the page was unusable in the browser

109 occurrences of "<? echo ... ?>" in web/index.php (48) and
web/briskin5/index.php (61). Short tags only work with short_open_tag = On,
which is Off by default in php and is Off on debian 13; on the production
machine (debian 8) it is evidently On.

This is not a cosmetic problem. Without interpretation the text of the tag
ends up literally in the html, and in a javascript context such as

    var g_tables_n = <? echo TABLES_N; ?>;

it becomes a syntax error that prevents the compilation of the WHOLE <script>
block. As a consequence none of the variables declared in there is created,
"sess" included, and the room page is unusable: the browser console reports
"sess is not defined" and the buttons do nothing.

Every src/href with cache busting was broken too
("commons.js?v=<? echo BSK_BUSTING; ?>"), and now renders properly
("commons.js?v=997ebdc").

Converted to <?php instead of turning short_open_tag on: the directive is
discouraged and not guaranteed, while the explicit form works everywhere.
All 109 occurrences had the identical shape "<? echo", and none of them fell
inside a php string, so the substitution is mechanical. Checked that the
generated page no longer contains uninterpreted tags.

Found by the user opening the site with a real browser: it is the first
defect that came from the javascript client, which the curl tests could not
detect because they do not execute the page.

NOTE: this commit also carries two pre-existing changes from the working
copy, unrelated to the tag conversion: the inclusion of custom.js in
index.php (two lines) and $brisk_donate passed to $brisk_vertical_menu. They
were already there and were picked up by the "git add" of the whole file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agophp8: "string + string" fatal in index_wr.php
Matteo Nastasi [Sun, 13 Sep 2026 10:31:35 +0000 (12:31 +0200)]
php8: "string + string" fatal in index_wr.php

    echo "Get User Error:" + $argz[0];

A concatenation written in javascript style, with "+" instead of the php ".".
On php 5 and 7 it evaluated to 0+0 with a warning and printed "0"; since
php 8 adding two non numeric strings is a TypeError, and with no catch
anywhere the brisk-spush daemon died on the spot.

The branch is trivial to reach: a request to index_wr.php with an
unrecognised session is enough, an expired cookie for instance. Found by
sending a getchallenge after a daemon restart.

All similar cases were looked for: this is the only one in php code, the
other "+" between strings are inside javascript embedded in the html, or in
shell scripts quoted in comments.

Found by playing a real game in the container: five authenticated users,
table 4, the auction, 40 cards played, score saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agofixes that showed up by actually running the application on debian 13
Matteo Nastasi [Sun, 13 Sep 2026 10:31:28 +0000 (12:31 +0200)]
fixes that showed up by actually running the application on debian 13

Found by bringing the whole stack up in a container: apache 2.4.68 with
mod_proxy_fdpass2, the brisk-spush daemon on php 8.4 with the ancillary
extension, postgresql 17. None of these was visible with the lint, with
loading the include chain, or with the tests on the objects: they only show
up by starting the daemon and serving a real request.

sac-a-push.phh: fatal when the daemon starts
 sig_handler() was registered with pcntl_signal() as
 array("Sac_a_push", "sig_handler"), that is in static form, but declared
 non static. Since php 8 that is no longer a valid callable and
 pcntl_signal() raises a TypeError: the daemon died before opening a socket.
 It is the same class of problem as the 15 static calls already fixed, but
 with the array() syntax: the check I had written looked for "Class::method"
 and did not see it. The other two callables in that form were checked as
 well (IPClassItem::compare and Cookie::create): both already static.

INSTALL.sh: Etc/ was born exposed on the web
 The Etc directory holds the configuration with $G_dbauth, that is the
 database credentials in clear, and it falls inside the DocumentRoot. The
 .pho extension is not associated with php, so the file was served as plain
 text: checked, HTTP 200 with the content. In production it is protected only
 because someone added a .htaccess by hand; a fresh installation was born
 without one. INSTALL.sh now creates it, in the apache 2.4 form with a 2.2
 fallback. After the change: HTTP 403.

WARNING.txt: the suggested ProxyPass lines did not work
 It is the text INSTALL.sh prints to the administrator as the configuration
 to write, and it was wrong in three ways:
  - "fd:///path" is refused at configuration time by apache 2.4.68
    ("ProxyPass URL must be absolute!"); "fd://localhost/path" is needed
  - it mentioned a single "brisk.sock", from before the pool existed: the
    path is the prefix and the module appends "<N>.sock" to it
  - the hardcoded path /var/www/brisk-priv ignored the -U option
 Rewritten with the form verified to work, plus the note that the first
 argument must be an exact path and not a prefix with a trailing slash.

The file also lists, and this part was already right, which urls go to the
daemon: index.php, index_wr.php, index_rd.php, index_rd_wss.php and the
matching ones under briskin5. Everything else is served by apache.

Final check in the container: GET /brisk/index.php answers 200 with the game
page (19725 bytes), the .css are served by apache, Obj/, spush/ and
briskin5/Obj/ answer 403, Etc/ answers 403, and the daemon does not emit a
single warning or deprecation while serving the requests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agopostgresql 10+ compatibility (debian 13 ships 17)
Matteo Nastasi [Sun, 13 Sep 2026 10:31:03 +0000 (12:31 +0200)]
postgresql 10+ compatibility (debian 13 ships 17)

Found by running the code against a real database: neither the lint nor
loading the sources could see them.

UPDATE ... SET (col) = (val)
Since postgresql 10 the parenthesised form on a SINGLE column is an error
("source for a multiple-column UPDATE item must be a sub-SELECT or ROW()
expression"): (val) is not a ROW but a parenthesised expression. The multi
column form is still valid, checked on the server: of the 11 parenthesised
UPDATEs in the project only 4 need fixing, the other 7 are left alone.

  dbase_pgsql.phh  SET (lintm)     user_update_login_time()
                   SET (pass)      user_update_passwd()
                   SET (tos_vers)  user_tos_update()
                   SET (game_cnt)  bin5_points_save()
  sql.d/085-tourn-update.sql  two SET (name)

This is not a consequence of the php 8 port: they were already broken on any
postgresql >= 10. They cover password recovery and the acceptance of the
terms of service.

int2four()
The literal 0xffffffff00000000 is above PHP_INT_MAX, so php treats it as a
float and the or converts it back to int: since 8.1 that is the "Implicit
conversion from float to int loses precision" deprecation, emitted on every
call (the function sits in the self-registration check path). Rewritten with
~0xffffffff, same bit pattern but an integer. Identical values, compared on
0, 1, 0x7fffffff, 0x80000000, 0xc0a80001 and 0xffffffff.

Checked against a real database (postgresql 17, schema rebuilt from scratch
with sql/builder.sh: 18 files, 12 tables, 6 views, 0 errors): connection,
queries, user_add, login_exists, getrecord_bylogin, the three fixed UPDATEs,
the two multi column ones, transactions and selfreg. No warnings, no
deprecations. The error branch of BriskDB::query() was checked too, by
forcing a query on a non existing table: it logs with pg_last_error(), does
not raise a TypeError, and the connection survives the recovery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoport to php 8.4 (debian 13)
Matteo Nastasi [Sun, 13 Sep 2026 10:30:35 +0000 (12:30 +0200)]
port to php 8.4 (debian 13)

The code was written for php 5. Minimal changes to make it run cleanly on
8.4, with no restructuring.

Fatal errors
- split() -> explode() (removed in 7.0), 5 places
- "$x =& new Class()" -> "= new" (removed in 7.0), 7 places
- 41 php4 style constructors -> __construct(). A non obvious case: Bin5_user
  defined "function User() {}", which on php5 was its constructor because it
  overrode the slot inherited from User; that one was renamed too.
- 15 static calls to non static methods (Challenges::load_data(),
  Hardbans::add(), Table::create(), ...): E_STRICT on php5, Error since 8.0.
  "static" added to the 8 declarations, none of them uses $this.
- 5 overrides with incompatible signatures (spawn, copy, load_step,
  unproxy_step, page_sync): E_STRICT on php5, fatal since 8.0. The useless
  "&" on objects were dropped and the parameters of three methods reordered,
  with the two call sites adjusted.
- dbase_pgsql.phh: pg_result_status($res) was called in the branch where
  $res is FALSE. Since 8.0 results are \PgSql\Result objects and no longer
  resources, so it is not a warning any more but a fatal TypeError - and in
  the connection recovery path, of all places. Replaced with pg_last_error().
- dbase_pgsql.phh: "${rules_name}::game_description(...)" was a variable
  variable whose name came from an undefined constant; on php5 it degraded to
  a string with a notice and resolved to $rules_name by accident, on php8 it
  is a fatal Error.
- dbase_file.phh: define() with an unquoted constant name, same mechanism.
- usermgmt.php: "break" outside any loop. On php5 it was a runtime fatal,
  since 7.0 it is a compile time one: the file did not load any more.

Deprecations
- 245 "var $prop" -> public
- 29 occurrences of "${var}" inside strings -> "{$var}" (8.2)
- 29 dynamic properties declared (8.2). User declared $brisk but the code
  always uses $room: renamed, nobody reads $user->brisk.
- 53 pg_numrows() -> pg_num_rows(): the alias is deprecated in 8.4
- strftime() -> date(), shmop_close() -> unset()
- room_join_wakeup(): removed a default followed by a mandatory parameter

mbstring.func_overload
It was set to 7 in the .htaccess files and was removed in 8.0. All 62 call
sites of strlen/substr/strpos were examined: they are either pure ASCII or
deliberately byte oriented, and moving to php8 fixes them, given that the
websocket frame parsing in transports.phh and the fwrite accounting in
sac-a-push.phh would have been wrong under overload. No change needed: where
character semantics were required the author already used explicit mb_*.
The only exception is index_wr.php, where mail() is no longer remapped onto
mb_send_mail(): mb_encode_mimeheader() was added on the subject and on the
user name, which otherwise ended up as raw UTF-8 in the headers.

Configuration (debian 13)
- .htaccess: func_overload removed, internal_encoding/http_input replaced by
  default_charset; the php_value block now sits inside <IfModule mod_php.c>
  because with PHP-FPM apache would answer 500
- the three .htaccess that protect the sources used the apache 2.2 syntax
  (Order/Deny), which needs mod_access_compat: now "Require all denied" with
  a fallback
- system/etc_php5_conf.d_mbstring.ini -> etc_php8.4_conf.d_brisk.ini
- INSTALL.sh: "php5 -l" -> "php -l"

Checked: php -l clean on 64 files; the whole include chain of the daemon
loads with E_ALL without warnings or deprecations; the tests in test/ pass.
Not yet verified against a database: dbase_pgsql.phh was only checked
statically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

4 weeks agoPHPMailer 5.1 replaced by a vendored 7.1.1
Matteo Nastasi [Sun, 13 Sep 2026 10:28:36 +0000 (12:28 +0200)]
PHPMailer 5.1 replaced by a vendored 7.1.1

The class.phpmailer.php in web/Obj/ was the 5.1 release from 2009 and does
not run on php 8: it uses each() (removed in 8.0),
get_magic_quotes_runtime() and set_magic_quotes_runtime() (removed in 8.0)
and php4 style constructors.

Replaced by PHPMailer 7.1.1 in web/Obj/PHPMailer/ (src/ plus the italian
language file and the LICENSE). 7.x was chosen over 6.x because 7.0.0 is
identical to 6.11.1: the major bump only signals a compatibility break for
those who extend the class (lang(), setLanguage() and $language became
static), and here PHPMailer is not extended. It is the line maintained for
php 8.4. No composer: the project does not use it, and INSTALL.sh already
copies files recursively - only LICENSE and VENDOR.txt had to be added to
the list of copied names.

mail.phh adjusted: namespace PHPMailer\PHPMailer, explicit require of the
three files (no autoloader), setFrom() instead of assigning From/FromName
directly.

A missing catch was added too: brisk_mail() builds PHPMailer with
exceptions=TRUE, so send() throws instead of returning FALSE, but none of
the 7 callers catches and all of them test for "== FALSE". A delivery error
killed the spush daemon. The exception is now logged and reported as FALSE,
which is what the callers already expected.

NOTE: msgHTML() overwrites AltBody with its own conversion of the html, so
the text passed to brisk_mail() is discarded. This was already the case
with 5.1, and the behaviour is left untouched (see the comment in the file).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

6 years agofix missing bold for your account item master
Matteo Nastasi [Tue, 12 May 2020 11:59:40 +0000 (13:59 +0200)]
fix missing bold for your account item

6 years agonb cards css added
Matteo Nastasi [Sun, 10 May 2020 12:56:56 +0000 (14:56 +0200)]
nb cards css added

6 years agoadd new (old) 'no border' deck
Matteo Nastasi [Sun, 10 May 2020 12:46:50 +0000 (14:46 +0200)]
add new (old) 'no border' deck

6 years agoversion update to 5.10.0 sprites v5.10.0
Matteo Nastasi [Tue, 5 May 2020 05:54:01 +0000 (07:54 +0200)]
version update to 5.10.0

6 years agotypo fixed
Matteo Nastasi [Mon, 4 May 2020 06:23:21 +0000 (08:23 +0200)]
typo fixed

6 years agoproper language management for deck reload (and close button)
Matteo Nastasi [Mon, 4 May 2020 06:22:09 +0000 (08:22 +0200)]
proper language management for deck reload (and close button)

6 years agorenamed close button
Matteo Nastasi [Mon, 4 May 2020 06:09:30 +0000 (08:09 +0200)]
renamed close button

6 years agoupdate img preload
Matteo Nastasi [Mon, 4 May 2020 06:07:24 +0000 (08:07 +0200)]
update img preload

6 years agotypo fixed
Matteo Nastasi [Mon, 4 May 2020 05:47:06 +0000 (07:47 +0200)]
typo fixed

6 years agoupdate cards_yy.css
Matteo Nastasi [Mon, 4 May 2020 05:41:28 +0000 (07:41 +0200)]
update cards_yy.css

6 years agocss for 'yy' deck added
Matteo Nastasi [Mon, 4 May 2020 05:32:29 +0000 (07:32 +0200)]
css for 'yy' deck added

6 years agoseeds chooser updated and dynamic empty card use
Matteo Nastasi [Mon, 4 May 2020 05:28:57 +0000 (07:28 +0200)]
seeds chooser updated and dynamic empty card use

6 years agoimages caching disabled
Matteo Nastasi [Mon, 4 May 2020 05:19:22 +0000 (07:19 +0200)]
images caching disabled

6 years agodynamic h and w adjusted
Matteo Nastasi [Sun, 3 May 2020 19:21:03 +0000 (21:21 +0200)]
dynamic h and w adjusted

6 years agog_deck test [2]
Matteo Nastasi [Sun, 3 May 2020 19:17:35 +0000 (21:17 +0200)]
g_deck test [2]

6 years agog_deck test
Matteo Nastasi [Sun, 3 May 2020 19:14:03 +0000 (21:14 +0200)]
g_deck test

6 years agotypo fixed in css
Matteo Nastasi [Sun, 3 May 2020 13:38:21 +0000 (15:38 +0200)]
typo fixed in css

6 years agopartial css spacing update
Matteo Nastasi [Sun, 3 May 2020 13:31:34 +0000 (15:31 +0200)]
partial css spacing update

6 years agofixed callerimg placement (and visibility) [2]
Matteo Nastasi [Sun, 3 May 2020 12:41:19 +0000 (14:41 +0200)]
fixed callerimg placement (and visibility) [2]

6 years agofixed callerimg placement (and visibility)
Matteo Nastasi [Sun, 3 May 2020 12:40:07 +0000 (14:40 +0200)]
fixed callerimg placement (and visibility)

6 years agoinvestigation about callerimg placement [2]
Matteo Nastasi [Sun, 3 May 2020 12:32:55 +0000 (14:32 +0200)]
investigation about callerimg placement [2]

6 years agoinvestigation about callerimg placement
Matteo Nastasi [Sun, 3 May 2020 12:24:53 +0000 (14:24 +0200)]
investigation about callerimg placement

6 years agotypo fixed
Matteo Nastasi [Sun, 3 May 2020 12:09:32 +0000 (14:09 +0200)]
typo fixed

6 years agotentative to fix dynamic callerid position
Matteo Nastasi [Sun, 3 May 2020 12:06:39 +0000 (14:06 +0200)]
tentative to fix dynamic callerid position

6 years agotypo fixed
Matteo Nastasi [Sun, 3 May 2020 12:00:12 +0000 (14:00 +0200)]
typo fixed

6 years agoupdate 'callerimg' placing
Matteo Nastasi [Sun, 3 May 2020 11:57:50 +0000 (13:57 +0200)]
update 'callerimg' placing

6 years agofixed callerimg set
Matteo Nastasi [Sun, 3 May 2020 11:49:59 +0000 (13:49 +0200)]
fixed callerimg set

6 years agodisabled class cleanup for played cards
Matteo Nastasi [Sun, 3 May 2020 11:40:31 +0000 (13:40 +0200)]
disabled class cleanup for played cards

6 years agomove all 'src' assign to 'data-card-id' logic
Matteo Nastasi [Sun, 3 May 2020 11:29:22 +0000 (13:29 +0200)]
move all 'src' assign to 'data-card-id' logic

6 years agohalf border resize
Matteo Nastasi [Sun, 3 May 2020 11:00:11 +0000 (13:00 +0200)]
half border resize

6 years agomicroadjust of north east player
Matteo Nastasi [Sun, 3 May 2020 09:12:26 +0000 (11:12 +0200)]
microadjust of north east player

6 years agomicroadjust of east player
Matteo Nastasi [Sun, 3 May 2020 09:01:04 +0000 (11:01 +0200)]
microadjust of east player

6 years agoremove border size in card dimension computation and code cleanup
Matteo Nastasi [Sun, 3 May 2020 08:47:57 +0000 (10:47 +0200)]
remove border size in card dimension computation and code cleanup

6 years agotypo fixed
Matteo Nastasi [Sun, 3 May 2020 08:43:28 +0000 (10:43 +0200)]
typo fixed

6 years agoadjust static variables with card dimensions
Matteo Nastasi [Sun, 3 May 2020 08:42:01 +0000 (10:42 +0200)]
adjust static variables with card dimensions

6 years agoassign sprites to owned cards
Matteo Nastasi [Sun, 3 May 2020 08:35:18 +0000 (10:35 +0200)]
assign sprites to owned cards

6 years agoremoved borders from sprited cards
Matteo Nastasi [Sun, 3 May 2020 08:05:56 +0000 (10:05 +0200)]
removed borders from sprited cards

6 years agofirst tranche of card images substitution
Matteo Nastasi [Sun, 3 May 2020 07:57:31 +0000 (09:57 +0200)]
first tranche of card images substitution

6 years agoadd new sp-card class to use with new sprites
Matteo Nastasi [Sun, 3 May 2020 07:52:16 +0000 (09:52 +0200)]
add new sp-card class to use with new sprites

6 years agosubstitute card id '40' with 'cover' to be more clear and back-compatible
Matteo Nastasi [Sun, 3 May 2020 07:32:16 +0000 (09:32 +0200)]
substitute card id '40' with 'cover' to be more clear and back-compatible

6 years agoadd css for cards 'xx'
Matteo Nastasi [Sun, 3 May 2020 06:38:36 +0000 (08:38 +0200)]
add css for cards 'xx'

6 years agoenabled deck select at table preferences
Matteo Nastasi [Sat, 2 May 2020 16:25:54 +0000 (18:25 +0200)]
enabled deck select at table preferences

6 years agoconsistency to add minute to apprentices tables minuta-appr
Matteo Nastasi [Tue, 28 Apr 2020 16:21:58 +0000 (18:21 +0200)]
consistency to add minute to apprentices tables

6 years agoadd match check on ttype to generate ranking just on certified and guaranteed users
Matteo Nastasi [Tue, 28 Apr 2020 16:06:17 +0000 (18:06 +0200)]
add match check on ttype to generate ranking just on certified and guaranteed users

6 years agoadd new ttype field to match table
Matteo Nastasi [Tue, 28 Apr 2020 15:48:45 +0000 (17:48 +0200)]
add new ttype field to match table

6 years agomissing g_listen entry substitution fixed
Matteo Nastasi [Sat, 18 Apr 2020 15:37:23 +0000 (17:37 +0200)]
missing g_listen entry substitution fixed

6 years agoupdate version
Matteo Nastasi [Sat, 18 Apr 2020 13:52:08 +0000 (15:52 +0200)]
update version

6 years agoISOLAUTH to 0x04 (test)
Matteo Nastasi [Sat, 18 Apr 2020 13:43:07 +0000 (15:43 +0200)]
ISOLAUTH to 0x04 (test)

6 years agomove to jquery 3.4.1
Matteo Nastasi [Sat, 18 Apr 2020 12:47:12 +0000 (14:47 +0200)]
move to jquery 3.4.1

6 years agomixed content issue fixed
Matteo Nastasi [Sat, 18 Apr 2020 12:42:02 +0000 (14:42 +0200)]
mixed content issue fixed

6 years agomixed content issue fixed
Matteo Nastasi [Sat, 18 Apr 2020 12:40:54 +0000 (14:40 +0200)]
mixed content issue fixed