website/ is hand-written HTML and CSS, uploaded as it is, like the MailifySMS site: SealShare as software a company installs for its own upload platform, how it works, features, a desktop/phone and light/dark gallery of the generated screenshots, a dated and sourced comparison with hosted transfer services and self-hosted tools, the Docker quick start, FAQ and a privacy page for Plausible. Colours come from the app's scheme, Google Sans Flex is served locally, nothing else loads from other hosts. The README shows three screenshots and no longer calls the encryption end-to-end. WebsiteTest guards missing files, other hosts, the screenshot set and the encryption wording. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
258 lines
19 KiB
Markdown
258 lines
19 KiB
Markdown
# Screenshots and the SealShare website
|
||
|
||
## Goal
|
||
|
||
Two things that feed each other. First, one command — `composer screenshots` — produces every
|
||
screenshot of SealShare from fixed demo data, desktop and phone, light and dark, ready for the web.
|
||
Second, a static website at **sealshare.nonameweb.ch**, made the way mailifysms.nonameweb.ch is:
|
||
hand-written HTML and CSS in `website/`, uploaded by hand. The site presents SealShare as what it
|
||
is — software a company installs to run **its own upload platform**, so it exchanges files with
|
||
customers securely without relying on an outside service — shows the screenshots, compares
|
||
SealShare with hosted transfer services and with other self-hosted tools, and tells how to install
|
||
it. It goes live with 2.0.0. The README gets a few of the same screenshots, and its encryption
|
||
wording is corrected.
|
||
|
||
## Context
|
||
|
||
**MailifySMS, the model** (`../MailifySMS`):
|
||
|
||
- `website/` holds `index.html`, `privacy_policy.html`, `terms_and_conditions.html`,
|
||
`css/theme.css` (a palette sampled from the app's screenshots), `css/device-frame.css` (a phone
|
||
bezel shared with the store canvases), self-hosted Poppins (`fonts/`, OFL) and `img/`
|
||
(`icon.png`, `hero.jpg`, `screenshots/{light,dark}/NN-name.png` at 540px).
|
||
- `index.html`: sticky nav with a phone toggle, hero, "How it works", "Key features", a screenshot
|
||
gallery with a Light/Dark switch (`data-light`/`data-dark` on each `<img>`), FAQ accordion,
|
||
contact card (`surtic86@gmail.com`), footer (quick links, legal). Plausible:
|
||
`<script defer data-domain="mailifysms.nonameweb.ch" src="https://plausible.io/js/script.js">`.
|
||
The page's JS is one inline `<script>` at the end.
|
||
- `CLAUDE.md` records that `website/` "is a faithful copy of what is deployed, images included, so
|
||
it can be uploaded wholesale". There is no deploy automation.
|
||
- `tools/screenshots.sh` (macOS only) drives an emulator and headless Chrome; documented in
|
||
`CLAUDE.md` § Screenshots, with the reasons behind each quirk.
|
||
|
||
**Hosting.** `*.nonameweb.ch` is a wildcard DNS record to `80.74.140.2` (METANET shared hosting,
|
||
nginx), the same as mailifysms. `sealshare.nonameweb.ch` resolves already; HTTP serves the host's
|
||
placeholder, HTTPS has no certificate. Creating the site and its Let's Encrypt certificate is done
|
||
in the hosting panel.
|
||
|
||
**SealShare.**
|
||
|
||
- Laravel 13.31, Livewire 4.4, Livewire Material 1.0.1, Pest 5.1 with `pestphp/pest-plugin-browser`
|
||
(Playwright 1.63). The browser tests run the app in-process, so factories, `Storage::fake()` and
|
||
`travelTo()` shape what the browser sees. `tests/Pest.php` applies `Tests\TestCase` and
|
||
`RefreshDatabase` to `Feature` and `Browser`, and creates an admin in `beforeEach` (the setup
|
||
gate).
|
||
- Device presets: `visit()->on()->macbook14()` is 1512×982 at 2× (a 3024×1964 capture);
|
||
`on()->iPhone15Pro()` is 393×852 at 3× (1179×2556). `inLightMode()` / `inDarkMode()`,
|
||
`screenshot(fullPage, filename)`. Screenshots are written to `tests/Browser/Screenshots/<name>.png`;
|
||
the directory is created, subdirectories in the name are not — names must be flat. Pest empties
|
||
that directory when a browser run starts, and has no reduced-motion emulation.
|
||
- Pest's in-process server does not store a multipart upload, so a browser test cannot select files
|
||
through the file input.
|
||
- PHP here has GD with WebP and PNG support; the production image does not need it (this is a
|
||
development tool).
|
||
- Colours: `resources/css/material-scheme.json` (seed `#4f46e5`, Vibrant) holds the light and dark
|
||
roles as hexes. Font: Google Sans Flex, `vendor/nonameweb/livewire-material/resources/fonts/google-sans-flex/GoogleSansFlex-Latin.woff2`
|
||
with its `OFL.txt`. Logo: `resources/views/components/app-logo-icon.blade.php` (SVG).
|
||
- **Encryption, as the code does it:** `ShareService::createShare()` encrypts each uploaded file on
|
||
the server with AES-256-GCM (chunked) through `FileEncryptionService`. Without a share password
|
||
the key is stored in `shares.encryption_key`; with one, the key is derived with PBKDF2-SHA256 and
|
||
never stored. The server sees the plaintext while uploading and downloading. The README calls
|
||
this "End-to-End Encryption", which it is not.
|
||
- The upload page is public, optionally behind the system password (`SystemPasswordGate`); a
|
||
customer given that password can upload and send the link back.
|
||
- Install today (README): `ghcr.io/surtic86/sealshare`, clone from GitHub. Gitea
|
||
(`gitea.nonameweb.ch/noNameWEB/SealShare`) is now the public repository; the image registry for
|
||
2.0.0 is settled separately.
|
||
- `.gitignore` does not ignore `tests/Browser/Screenshots`; `.dockerignore` excludes `tests` and
|
||
`*.md` but would copy a `website/` directory into the image.
|
||
|
||
**Peers.** Pingvin Share has been archived since June 2025 (its README points to forks such as
|
||
Pingvin Share X). PsiTransfer, Gokapi and Erugo are single-purpose self-hosted share tools; Gokapi
|
||
advertises end-to-end encryption.
|
||
|
||
## Decisions
|
||
|
||
- **Positioning: software you host, not a service** — the site says plainly that SealShare is not
|
||
hosted by anyone but the company that installs it: its own upload platform for exchanging files
|
||
with customers, data on its own server, no dependence on an external service.
|
||
- **Pages: `index.html` and `privacy.html`** — one landing page, and a short privacy page because
|
||
the site uses Plausible. No terms page: the software is MIT-licensed and no service is offered.
|
||
- **A comparison with hosted transfer services and with self-hosted share tools** — two tables on
|
||
the landing page. Cloud suites (Nextcloud-style) are left out.
|
||
- **Hand-written HTML and CSS, like MailifySMS** — no build step; `website/` is uploaded as it is.
|
||
- **Colours copied from `material-scheme.json`, not sampled** — `website/css/theme.css` lists the
|
||
roles it uses with the scheme's hexes, light by default and dark under
|
||
`@media (prefers-color-scheme: dark)`; it names the seed and variant it was copied from, so a
|
||
regenerated scheme is copied again. The site follows the visitor's system theme and has no
|
||
toggle of its own.
|
||
- **Google Sans Flex, self-hosted** — the app's font, copied with its `OFL.txt` into
|
||
`website/fonts/`; nothing is loaded from Google.
|
||
- **Uploaded by hand, like MailifySMS** — `website/` is a faithful copy of what is live. You create
|
||
the subdomain and certificate once in the hosting panel and upload the folder when it changes.
|
||
No hosting credentials anywhere in the repository or CI.
|
||
- **Plausible** — `data-domain="sealshare.nonameweb.ch"`, the same script as MailifySMS; the site
|
||
must be added in the Plausible account.
|
||
- **English only.**
|
||
- **Screenshots: desktop and phone, each in light and dark (20 images)** —
|
||
desktop (MacBook 14, 2×): `01-upload` (files selected, options filled), `02-share-created`,
|
||
`03-qr-code` (the dialog), `04-download` (the recipient's file list), `05-admin-dashboard`,
|
||
`06-admin-settings`; phone (iPhone 15 Pro, 3×): `01-upload`, `02-password` (the recipient's
|
||
password prompt), `03-download`, `04-qr-code`.
|
||
- **Screenshots run as Pest browser tests in `tests/Screenshots/`, started by `composer screenshots`**
|
||
— the directory is not one of phpunit.xml's test suites, so `php artisan test`, the Browser
|
||
suite and CI never run it. It reuses Playwright and the in-process server.
|
||
- **Fixed demo data** — factories and `ShareService` with fixed names, sizes and tokens, time
|
||
frozen with `travelTo()`, the site title and branding at their defaults, one admin
|
||
("Alex Morgan"). Every run produces the same images unless the UI changed.
|
||
- **Images published as WebP by the test run itself** — after each capture a small helper resizes
|
||
it with GD into `website/img/screenshots/{desktop,phone}/{light,dark}/NN-name-<width>.webp` at two widths
|
||
(desktop 1600 and 800 px, phone 1080 and 540 px) for `srcset`. Raw PNGs stay in
|
||
`tests/Browser/Screenshots/`, which is gitignored; only the WebP files are committed.
|
||
- **Device frames in CSS** — `website/css/device-frame.css` draws a laptop and a phone around the
|
||
screenshots; the hero shows the desktop upload and the phone download screenshots framed, in
|
||
the visitor's theme. No generated hero image.
|
||
- **README shows three screenshots** — desktop upload, desktop share created, phone download (light),
|
||
referenced from `website/img/screenshots/…`, so each image exists once in the repository.
|
||
- **Encryption is described accurately, on the site and in the README** — "encrypted at rest with
|
||
AES-256-GCM; with a share password the key is never stored". The comparison marks end-to-end
|
||
encryption "no" for SealShare. The README's "End-to-End Encryption" line is corrected.
|
||
- **Built on `material`, live with 2.0.0** — screenshots show the 2.0.0 interface; the site links
|
||
the Gitea repository, and its install commands are the README's at release, whatever registry
|
||
2.0.0 ships with.
|
||
- **Comparison facts are researched, dated and sourced** — from each product's own site,
|
||
documentation or repository; the tables say "as of <month year>" and link every source. Hosted
|
||
services: WeTransfer, SwissTransfer, Dropbox Transfer, Google Drive links. Self-hosted tools:
|
||
open source, single-purpose, installable with Docker, with a release in the 12 months before the
|
||
research — expected Pingvin Share X, PsiTransfer, Gokapi and Erugo; any that fails the rule is
|
||
dropped and named in the commit message. Criteria (rows): where files are stored, who operates
|
||
it, recipient needs an account, password protection, expiry, download limit, encryption at rest,
|
||
end-to-end encryption, folder upload, custom branding, maximum file size, licence and cost. A
|
||
value that cannot be sourced is "—", never guessed.
|
||
|
||
## Out of scope
|
||
|
||
- A terms page, a documentation section, a German version.
|
||
- Deploy automation (Gitea Actions, SFTP scripts), and creating the subdomain, certificate or
|
||
Plausible site — those are yours in the hosting panel and the Plausible account.
|
||
- Comparing with Nextcloud, ownCloud or other cloud suites.
|
||
- Store-style canvases with captions, a generated hero image, video or animated screenshots.
|
||
- Running the screenshots in CI, or checking them against earlier runs (visual regression).
|
||
- The image registry move and the install commands' final form (settled with 2.0.0).
|
||
- Implementing end-to-end encryption.
|
||
|
||
## Implementation steps
|
||
|
||
1. **Housekeeping.** `.gitignore`: `/tests/Browser/Screenshots`. `.dockerignore`: `website`.
|
||
2. **Screenshot helper.** `tests/Screenshots/Publisher.php` (`Tests\Screenshots\Publisher`):
|
||
`publish(string $capture, string $device, string $theme, string $name, array $widths): void`
|
||
reads `tests/Browser/Screenshots/<capture>.png` with GD, and for each width writes
|
||
`website/img/screenshots/<device>/<theme>/<name>-<width>.webp` (quality 82, aspect kept,
|
||
directories created). It throws when the capture is missing, so a failed shot fails the run.
|
||
3. **Demo data.** `tests/Screenshots/DemoData.php`: `admin()`, `shares()` (eight shares with fixed
|
||
tokens, file names such as `Q3-report.pdf`, `Contract 2026.pdf`, `Product photos/…`, sizes,
|
||
download counts and expiries, one password-protected, one expired), created through factories
|
||
and `ShareService` with `Storage::fake('shares')` so the files exist encrypted.
|
||
4. **Screenshot tests.** `tests/Screenshots/ScreenshotsTest.php`, with `tests/Pest.php` extended to
|
||
`->in('Feature', 'Browser', 'Screenshots')`:
|
||
- `beforeEach`: `config(['session.driver' => 'file'])`, `travelTo('2026-10-01 09:30')`, demo
|
||
data; a `ready()` wait as in `SealShareTest`.
|
||
- One test per device and theme (four tests), each visiting the pages in turn, waiting for
|
||
`networkidle` and fonts (`document.fonts.ready`), hiding the text caret, capturing
|
||
viewport-sized (not full-page) shots, and calling `Publisher::publish()` right after each.
|
||
- The upload shot with files selected: create Livewire temporary uploads on the fake
|
||
`livewire-tmp` disk and set the uploader's property through `$wire.$set` with
|
||
`livewire-file:` references, then fill the options. If Livewire refuses that, the shot shows
|
||
the drop zone with the options filled instead, and the plan's risk note is updated.
|
||
- QR dialog: `click('[data-test="show-qr-code"]')`; password prompt: the protected share on the
|
||
phone; admin pages as the admin.
|
||
5. **Command.** `composer.json` script `"screenshots"`: `Composer\\Config::disableProcessTimeout`,
|
||
`npm run build`, `@php vendor/bin/pest tests/Screenshots` — the build first, so the shots show
|
||
the current assets. Playwright's Chromium must be installed (`npx playwright install chromium`),
|
||
as for the browser tests.
|
||
6. **Website scaffold.** `website/`:
|
||
- `css/theme.css` — tokens copied from `material-scheme.json` (with seed and variant noted),
|
||
Google Sans Flex `@font-face`, layout, nav, hero, sections, cards, tables (scrolling
|
||
sideways on a phone), FAQ (`<details>`), footer; light and dark through
|
||
`prefers-color-scheme`.
|
||
- `css/device-frame.css` — laptop and phone frames.
|
||
- `fonts/GoogleSansFlex-Latin.woff2`, `fonts/OFL.txt`; `img/logo.svg` (from `app-logo-icon`),
|
||
`img/icon.png` (favicon, from `public/`).
|
||
7. **Landing page.** `website/index.html` (Plausible in `<head>`, one inline script at the end):
|
||
- nav: Why, Features, Screenshots, Compare, Install, FAQ, Gitea;
|
||
- hero: "Your own secure upload platform" — self-hosted file exchange with customers, no
|
||
outside service; buttons "Install" (to #install) and "Source on Gitea"; framed desktop and
|
||
phone screenshots as `<picture>` elements whose `<source media="(prefers-color-scheme: dark)">`
|
||
picks the dark captures;
|
||
- "Why run your own": your server, your domain and branding, customers upload and download
|
||
without accounts, encrypted at rest, no per-seat pricing;
|
||
- "How it works": upload → link or QR code → the customer downloads, with expiry, download
|
||
limit and password;
|
||
- features (from the README, accurate encryption wording);
|
||
- screenshots: Desktop/Phone and Light/Dark switches over one gallery (Light/Dark starting on
|
||
the visitor's system theme), `srcset` for both widths, `loading="lazy"`, descriptive `alt`;
|
||
- compare: the two dated tables with sources (step 8);
|
||
- install: the README's Docker quick start and a link to the full instructions on Gitea;
|
||
- FAQ: "Is it end-to-end encrypted?" (no — at rest, and what a password adds), "Can customers
|
||
send files to us?" (yes, through the upload page, optionally behind the system password),
|
||
"How big can files be?" (the README's large-file limits), "What does it cost?" (MIT, your
|
||
hosting), "Who runs it?" (you);
|
||
- contact (`surtic86@gmail.com`, as MailifySMS) and footer (Gitea, licence, privacy, noNameWEB).
|
||
8. **Comparison research.** For each product, record every criterion with its source URL and the
|
||
date checked; apply the self-hosted selection rule; fill the tables. Keep the notes in the
|
||
commit message, not in the repository.
|
||
9. **Privacy page.** `website/privacy.html`: who runs the site (contact), the host (METANET, server
|
||
logs), Plausible (cookieless, no personal data, EU-hosted, link to its data policy), no other
|
||
third parties, fonts served locally, contact for questions; dated.
|
||
10. **README and changelog.** Correct the encryption lines (the intro sentence stays accurate;
|
||
"End-to-End Encryption" becomes "Encryption at Rest", described as in Decisions), add a
|
||
Screenshots section with the three images, add the website link. CHANGELOG `2.0.0` "Fixed":
|
||
the README no longer calls the encryption end-to-end. The website and the screenshot tooling
|
||
get no changelog entry — they do not change the application.
|
||
11. **Project notes.** `record-rule` for `website/**`: `website/` is a faithful copy of what is live,
|
||
uploaded by hand; its colours are copied from `material-scheme.json` and must be copied again
|
||
when the scheme is regenerated; the comparison is dated and every value sourced. And for
|
||
`tests/Screenshots/**`: run with `composer screenshots` whenever the interface changes, before
|
||
a release; the demo data is fixed so runs are reproducible.
|
||
|
||
## Testing
|
||
|
||
- `tests/Unit/ScreenshotPublisherTest.php`: a generated PNG is written as WebP at each requested
|
||
width with the aspect ratio kept, into the device/theme directory; a missing capture throws.
|
||
- `tests/Feature/WebsiteTest.php` guards `website/` without a browser:
|
||
- every local `src`, `href`, `srcset` entry and CSS `url()` resolves to a file in `website/`;
|
||
- every screenshot the gallery or README references exists for both widths and both themes;
|
||
- no request goes to a host other than `plausible.io` (no Google Fonts, no CDN);
|
||
- `index.html` and `privacy.html` have a `<title>`, `lang="en"` and a meta description;
|
||
- the README has no "End-to-End Encryption" line, the site's features section does not say
|
||
"end-to-end", and SealShare's end-to-end cell in the comparison (marked
|
||
`data-compare="sealshare-e2e"`) reads "No" — the FAQ may still ask the question.
|
||
- The screenshot run itself is the test of step 4: it fails when a page, selector or capture
|
||
breaks. It is run by hand before a release, not in CI.
|
||
- The site is looked at in Chrome, Firefox and Safari, light and dark, at phone width, before
|
||
uploading.
|
||
|
||
## Risks and open questions
|
||
|
||
- **Selecting files in the upload shot** works: the files are stored with Livewire's own
|
||
`FileUploadConfiguration::storeTemporaryFile()` and handed to `_finishUpload` by their signed
|
||
names; Livewire's temporary-upload cleanup is turned off for the run, because under the frozen
|
||
clock it deletes them.
|
||
- **Comparison accuracy and fairness.** Other products change; the tables are dated and sourced,
|
||
and re-checked when the site is updated. Swiss unfair-competition law expects comparisons to be
|
||
accurate and not misleading — values that cannot be sourced stay "—".
|
||
- **Install commands depend on the registry move.** Until it is settled, the install section copies
|
||
the current README; it is updated before the site goes live with 2.0.0.
|
||
- **Screenshot determinism.** Relative dates ("in 3 days") depend on `travelTo()`; animations
|
||
(the share-created shape, counting stats, dialog entry) are waited out — Pest has no
|
||
reduced-motion emulation — by waiting on `document.getAnimations().length === 0` before each
|
||
capture.
|
||
- **The host's name in the privacy page** (METANET) is inferred from the server's reverse DNS
|
||
(`urbanus.ch-meta.net`); confirm it before the page goes live.
|
||
- **The contact address** is the one MailifySMS publishes (`surtic86@gmail.com`); change it in
|
||
step 7 if SealShare should have its own.
|
||
- **Image weight.** Twenty screenshots at two widths as WebP should stay under ~4 MB in total; if
|
||
not, lower the quality or drop the larger phone width.
|
||
- **Colours drift** when the scheme is regenerated; the rule in step 11 and the note in
|
||
`theme.css` are the guard.
|