composer screenshots runs Pest browser tests in tests/Screenshots, outside every test suite: fixed demo data under a frozen clock, desktop (MacBook 14, 2x) and phone (iPhone 15 Pro, 3x) in light and dark, each capture published at once as WebP at two widths into website/img/screenshots. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
256 lines
18 KiB
Markdown
256 lines
18 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** relies on Livewire accepting `livewire-file:` references set
|
||
from the browser; if it does not, that shot shows the empty drop zone with options (step 4).
|
||
- **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.
|