7 Commits

Author SHA1 Message Date
ignacionelson 073101d184 Put a ceiling on a zip download, and clean up after the ones that fail
Follow-up to #1687, which made a zip build report failure honestly. Four
things it passed near, none of them regressions it introduced.

A zip has never had a size limit — only a cap of 10,000 files, which
bounds nothing that costs anything. Ten thousand spreadsheets zip in
seconds; two hundred videos is an hour of stream-copying and an archive
that fills the disk. Bytes are what a build actually costs, so the new
Settings → Downloads screen caps the total size instead, at 2 GB out of
the box. It is a setting rather than a constant because the safe figure
depends on free disk, on whether sources live on a remote disk, and on
the plan a hosted tenant is on — the file count stays fixed, since it is
a foot-gun rail and not a knob anybody needs. The controller measures
the selection at request time and names both numbers when it refuses;
the job measures again, because it re-derives the selection at run time
and a folder can grow while the job waits in the queue.

Every shipped topology runs exactly one queue worker, and everything
shares the default queue, so raising the job timeout to an hour handed
any signed-in person an hour of everyone else's notification mail. There
is now one build in progress per requester and a named throttle bucket
on the endpoint, which had neither. A pending row older than an hour is
treated as abandoned rather than in progress, so a worker killed hard
enough to skip failed() cannot lock somebody out for good. Giving zip
builds their own queue is the structural fix and wants its own change:
it touches compose, supervisord and the systemd unit in INSTALL.md, and
an install that upgrades without changing its worker command would stop
building zips silently.

zip_downloads.requested_by cascades on delete, so removing a user takes
their rows with it and strands every archive they built — invisible to a
purge that walks rows, and to OrphanFileScanner, which skips zips/ on
purpose. The purge now also sweeps files in zips/ that no row explains,
after a day's grace so a build in progress is never taken out from under
itself.

Two smaller things while in here. A build that failed because every file
had already hit its download limit said only that nothing was available,
and dropped the skipped list — the same distinction the store guard goes
out of its way to draw at request time. And a failed close() now logs
libzip's reason, which the @ silencing had been discarding: "the disk is
full" and "the source vanished" are different problems for whoever has
to fix one, while the requester still sees a message with no server
paths in it.
2026-08-25 21:44:27 -03:00
denkfabrik-li 933eaa2ba4 Send mail through Microsoft Graph as an admin-connected mailbox
Adds "Microsoft 365 (OAuth)" to the Email settings provider dropdown.
Selecting it swaps the SMTP form for an app registration (client id,
secret, optional tenant) and a "Connect mailbox" flow: the admin signs
into the mailbox the installation should send as, and outgoing email
goes through Graph sendMail as that mailbox — no password, no app
password, no SMTP AUTH, which Microsoft is winding down.

Delegated flow on purpose: it needs no admin consent and works for
work/school and personal accounts alike. Its one weakness — a grant
can die silently behind a password reset or a Conditional Access
change — is answered by a daily scheduled refresh that keeps the
token alive and, on a dead grant, warns the settings admins once
in-app and on the settings page instead of letting mail stop quietly.

Tokens and the client secret live encrypted in their own row and are
read fresh at send time, never through the boot-config cache. The
stored SMTP transport survives a provider switch untouched.
2026-08-23 22:46:24 +02:00
ignacionelson cab9291d29 Stop two tables from growing forever on an untended installation
Failed queue jobs and read notifications both grow with use, and neither
ever shrank on its own. The failed-jobs list waited for somebody to press
"Delete all failed" — a fine tool for a backlog you are looking at, and
the only thing that ever emptied it. Notifications had nothing at all: one
row per recipient per event, kept for the life of the installation, on
what is easily the fastest-growing table here.

Both now have a retention window, set together on the Scheduler screen
under Housekeeping, and a nightly purge that honours it. Thirty days for
failed jobs and ninety for read notifications, and zero means keep
everything — the explicit choice somebody makes when a failure is evidence
rather than debris.

Unread notifications are never deleted, whatever their age. A notification
nobody has looked at is the one row in that table still doing its job, and
somebody back from four months away should find their news rather than a
clean slate. The activity log is untouched by any of this: it is an audit
trail, and it is never pruned.

Two things came out of building it. The API request log purge has been
running nightly since it shipped without ever appearing on the Scheduler
screen — so a failure of it was invisible on the screen that exists to
make failures visible — and there is now a test asserting the screen's
list and the schedule are the same list, because they had already drifted
once and would again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:44:06 -03:00
ignacionelson 997debc6a3 Let somebody ask for an update instead of waiting for tonight
The check ran daily and there was no other way to run it. An administrator
who has just read that a release fixes the thing bothering them had to
reach a terminal — or wait until tomorrow to be told what the project
announced this morning.

There is now a Check now button beside the setting that schedules it. It
says what came back: the version waiting, or that this installation is
already on the newest. The time of the last check sits next to it, because
the notice on the dashboard is only as good as when it was last refreshed
and nothing said when that was.

Deliberately not gated on the daily-check setting. Switching that off says
"do not have my server phone out unattended", which is not the same
sentence as "refuse to answer when I ask" — so the button works either way
and the setting keeps governing only the schedule.

The work moved out of the command into CheckForUpdates, because the part
that must not drift between the two callers is the part with consequences:
which staff get notified, and the guard that stops them being notified
again for a release they already know about. A second copy of that in a
controller would have been found wrong six months later by somebody
receiving the same notification every time a colleague pressed a button.

Two throttles, and the second is not redundant. The route's bucket is per
user; GitHub's limit is per server address, so two administrators each
within their own allowance can still exhaust the installation's. The
cooldown is installation-wide and costs no new setting — it reads the
timestamp every check already writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:35:48 -03:00
ignacionelson 4ce6793da9 Show a new installation's administrator around, once
Setup ended by handing somebody a login form and an empty dashboard.
Everything this application can do was one menu away, and which menu was
theirs to discover.

The first time the administrator signs in to a new installation they now
land on a short ordered list of what is worth doing first — add a client,
upload a file, group the people who get the same things, choose how the
file lists and the email look, point it at a mail server, add the team,
check the scheduler — each a link straight to the screen that does it.

The list is filtered twice, and both filters matter. By permission,
because a link that answers 403 is worse than no link. And by edition:
a managed installation is not sent off to configure a mail server
somebody else runs, to create staff accounts that are not its to create,
or to check a scheduler it does not host. Those three drop out on Cloud
and the other five remain.

Two steps tick themselves, because the database can answer them: a client
exists, a file exists. Nothing else is checkable without guessing — a
theme that was never changed looks exactly like one chosen deliberately —
and a tick meaning "we assume so" is worse than no tick.

The invitation to the Discord is at the very bottom, after the list.
Somebody who has just installed this came with a job in mind, and opening
with a social invitation is the fastest way to lose them.

The marker is raised where a first administrator comes into existence —
the setup screen and `projectsend:admin`, so a container provisioned from
environment variables is welcomed too — and it is false by default, so an
installation that updates into this feature is not congratulated on an
install it finished a year ago.

RedirectToWhatsNew becomes RedirectToGreeting and answers for both: they
are the same interruption, and a second middleware on the same route
would have to know about the first to avoid arguing with it. Installing
wins; release notes for a version you never ran are the wrong greeting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:44:22 -03:00
ignacionelson 6ddfc1aa5d Greet the administrator once, on the first visit after an update
An update finished and nothing said so. The dashboard looked identical to
yesterday's, and whatever the release brought was in a file nobody opens.

The first time the installation's administrator opens ProjectSend after
an update, they now land on a page that names the version they are on,
invites them to the Discord — the same invitation update.sh prints, made
again where they are actually looking — and then lays out what the
release brought.

The notes come from CHANGELOG.md inside the release, not from GitHub: the
one moment this page exists for is the moment after an update, possibly
on a server with no outbound access, describing code already on disk.
Parsed rather than rendered, so nothing in it can become HTML.

Once, and to one person. The update happened to the installation, so
greeting five staff members — each having to dismiss a page they did not
ask for — would turn a pleasant moment into a support question. It goes
to the oldest active administrator, which on any installation that went
through setup is whoever set it up. No owner flag was invented for this:
administrators are equal in authority, and changing that for a greeting
is not a trade worth making.

Only forwards, and only for a real update. A fresh install has nothing to
catch up on, a container reboot has not updated anything, and somebody
restoring an older release is dealing with a problem rather than
celebrating. Managed installations never see it at all — nobody signed in
there performed the update it thanks them for, which is the same gate the
System card and About's environment block already carry.

The redirect is attached to the dashboard alone, not the web group: it
catches a login and the sidebar logo both, without ever interrupting a
download to congratulate somebody. Reading the page clears the marker,
but the address keeps working — closing it by accident should not be
unrecoverable — and About now links to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:22:17 -03:00
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00