Take the website's screenshots with one command
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
4470c2ed87
commit
e49bdf3eb3
@@ -0,0 +1,255 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user