Rueda de Actos has a README. It's long, half technical documentation and half user manual, and it's accurate: the data model, every feature, where the backups live, what to do after a bad close.
Nobody who uses the app will ever read it.
That isn't a complaint about users. It's the most important design constraint the app has, and it took me two versions to treat it as one.
Rueda de Actos is the Windows desktop app I built for a festival group in Alcoy, Spain: five events, eleven spots each, and a rotating turn that has to stay fair year after year. The person who runs it is a volunteer on the group's board. They aren't technical, they open the app once a year, and on the night that matters there's no store to reinstall from, no support line and nobody to call. For most of the year I'm not in the room.
So every instruction that matters has to be somewhere they'll actually look, at the moment they need it. This is the last article in the series about this app, and it's about the unglamorous part: getting software into the hands of people who will never read the docs, and keeping it working there.
Before the app exists: the installer
The first moment is the only one where the app can't speak for itself, because it isn't running yet.
The installer is a standard Windows setup wizard, in Spanish, the language of the people who use it. And it isn't code-signed, so the first time it runs, Windows shows a blue warning: Windows protected your PC. The button that runs it anyway is hidden behind a More info link.
I went through the economics of a signing certificate in an earlier article. What matters here is what that screen does to someone who isn't technical. It reads as "this is a virus", and the software has failed before it has opened.
Nothing inside the app can fix that. So the explanation travels with the file: before anyone double-clicks, the person who will install it is told that a blue warning is coming and exactly which two things to press, in the words Windows will show them, Más información and then Ejecutar de todas formas.
The difference is all in the timing. A warning you didn't mention looks like a problem. A warning you predicted looks like proof that the instructions were right, and that buys trust for everything that comes after.
The first screen: an empty state that says what to do
The database is created empty. There's no demo data and no sample list. An earlier build seeded the table from a bundled file, and it turned out to fire whenever the table was empty, not only on the first run, so it went.
An empty table is a dead end, though. A grid of headers with nothing under it doesn't tell you what to do next. So when there's nobody in the list, the table isn't shown at all. In its place, translated from Spanish:
No members yet
Load the full list with Import, in the header, or add them one by one with Add member.
The event buttons stay disabled until there's someone to choose from. In an earlier build they didn't, and starting an event on an empty list produced empty backups and blocked the rota.
An empty state is the first page of the manual, and it's the only page that's guaranteed to be read.
Getting the data in: their spreadsheet, and errors that name the row
Every association already has a spreadsheet. The alternative to importing it is typing everyone in by hand, which is how a tool dies before its first use.
So Import takes the spreadsheet the group already had, Excel or JSON, with a single condition: the first row holds the headers, and one of them is Nombre. Headers are matched with or without accents, by their visible or internal name, abbreviations are accepted, and columns the app doesn't know are ignored. The details of that leniency are in the September article on real use.
What matters for people who won't read the docs is what happens when the file is wrong. If any record fails validation, the whole import is rejected, the data already in the app isn't touched, and the message says how many records failed and which rows, with the reason for each.
All or nothing is deliberate. A failed import is obvious. A half-done import looks exactly like a list, and nobody counts the rows to check that they all arrived.
And then the step that would be in bold in any manual: after importing, you have to press Save. Which brings me to the one rule in this app that has to be impossible to miss.
One button that makes it real
The data model is strict on purpose. The app always starts from the last saved state, and Save is the only thing that commits anything. Closing without saving is a clean undo of the whole evening, which is exactly what you want after a session that went sideways.
A rule like that is invisible unless the interface shows it, so it shows it all the time. The status bar along the bottom always says one of two things, Saved or Unsaved changes, and hovering the second one tells you to press Save. The comment above that element is the whole argument:
<!-- What isn't saved is lost on close, so the state has to be on screen at
all times, not just in the exit warning. -->
The exit warning exists too. Closing with something pending asks first, with three buttons: Save and exit, Exit without saving, Cancel. Clicking outside doesn't dismiss it. And if saving fails, the app stays open:
// If Save fails, we don't carry on: that would be exactly the case where it hurts most.
if (result.isConfirmed) return guardarDatos()
return result.isDenied === true
For the close that happens anyway (the wrong button, a flat battery, someone tidying up the cables), there's an escape hatch: Settings → Data management → Load temporary version (session) brings back the last thing that was on screen, saved or not, with the date of each version next to it.
None of that is documentation in the usual sense. But the status bar, the three buttons and the recovery option explain the data model better than any paragraph could, and they do it at the only moments the user is thinking about it.
Backups that live on the same disk
The app backs itself up: a full copy of the database every thirty minutes, keeping the last ten. It's a real safety net, for one specific failure: I've just broken the data.
It does nothing for the other one, the laptop died, because those copies sit on the same disk as the original. I said that in the offline-first article, and it bears repeating, because automatic backups are exactly the kind of feature that makes people stop worrying, and this is the one case where they should keep worrying.
What actually protects the group's history is an export saved somewhere else: a USB stick, an external drive, an email. JSON and Excel both re-import without losing anything, so with that file a brand-new installation gets everything back through the same Import as on the first day.
The trouble is that nobody thinks about this until it's too late, and nobody reads the paragraph telling them to. So the export sits where the data is worth the most. The final summary, the screen that appears when the fifth event closes, has the export buttons on it, and the highlighted one is the JSON export. At that moment the evening's work is done, someone is still at the keyboard, and a copy is one click away.
Updating without losing a year
The database doesn't live next to the program. It's in the user's Windows profile, so installing a new version over the old one never touches it. What does change is its shape, and the database a new version finds is, by design, a year old.
So the schema is versioned. Every change to it ships as a migration that runs on first launch, each one inside a transaction, so a failure stops the chain instead of leaving the database half converted. The migrations that move existing data have their own tests, because the database they will meet on that machine is the real one, with years of history and no copy on any server. I wrote about how those tests work in the testing article.
From the user's side, all of that has to collapse into a sentence they already believe without being told: the new version keeps my data.
A year later: the guide
The hardest user to design for is the same person, twelve months later. They used the app once, last year, and have forgotten all of it: where Import is, what the amber card means, why some cells are striped.
For that person, the app carries its own guide, one click away inside the app: nineteen short numbered sections, from managing members and choosing people for an event to verifying or skipping, reopening, the gender rules, squad leaders, search, saving and recovering, export and import, the final summary and how windows and notices behave.
A few rules make it usable rather than merely complete:
- It uses the screen's words. Every button is named exactly as it appears, in bold, and every path is the real one: Settings → Data management → Load temporary version (session).
- Each section is a handful of short sentences. Nobody reads a guide from the top. People scan it for the one thing they're stuck on.
- It changes with the app. When version 2 added search, reopening and the new event states, the guide grew in the same release. A feature isn't finished until its section is written.
A guide inside the app has one advantage over every other kind of documentation: it's the only one guaranteed to still be there, on the right machine, a year from now.
The interface is the documentation
Look at where the instructions in this app actually live: a heads-up that travels with the installer, an empty state, an error that names the rows, a status bar, a dialog with three buttons, the export buttons on the last screen of the night, and a guide written in the words of the screen.
That's the documentation. For someone who isn't technical, the documentation that exists is the interface itself. Everything else, the README, the FAQ, this article, gets read by people who no longer need it.
This is the last article I'll write about Rueda de Actos for a while. The case study, with screenshots made from test data, is on my site: ancer.es/en/project/rueda-ligeros.
Top comments (2)
Putting export on the fifth-event summary is a good moment to ask for the off-disk copy. One restore fixture I'd add is: export immediately after closing event five with unsaved changes, then import that file into a fresh Windows profile. Does it reproduce the visible completed rota, or the last saved state? With Save as the only commit, that distinction needs to be explicit before someone treats the export as their year's backup. I'd compare history and the next rotation as well as member counts, since a restore that looks populated can still lose the fairness state.
Good fixture, and it found real gaps.
Visible or saved? Visible. Export writes the list in memory, the one on screen, not what's in SQLite. Export from the summary before saving and the file holds the completed rota even if you then choose Exit without saving. Import doesn't commit either: it replaces the working list and you still press Save. For a backup I think exporting what you see is right, but you're right that it's implicit: nothing on the summary says so.
Fairness state. The rotation is the list order plus, for each member, the year they last did each event, the year before that, and who was squad leader. Those are all per-member fields, so they travel, and the next turn comes from the same order. A scenario test already checks that field by field after a round trip through an empty database, not by counts.
What doesn't travel. Two things live only in the database:
So the next update changes three things: exporting with unsaved changes asks first, the file carries the festival year and which events are closed, and the summary falls back to each member's own squad-leader flags when an event has no snapshot. Your fixture is now a test, as you wrote it: unsaved changes after event five, one event closed short and one skipped, an empty profile a year later, then history, which event is open and who goes next.