Build MaintenEase
KINDAreplaces $49/mosaves $588/yrback to the verdict
A single-site maintenance tracker whose hard problems are recurrence that neither duplicates nor avalanches, meter readings that go backwards when a gauge is replaced, and an append-only service history that is still readable in ten years.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server, the scheduler and the test suite.
Get it nodejs.org, or your package manager. open ↗
Verify
node --version prints v22 or higher - accounta few pounds a month, or free on hardware you already run
Why The scheduler has to run whether or not anyone is logged in, and a preventive plan that only generates when someone opens a browser is not a plan.
Get it A small virtual machine or a box in the plant office, with the service starting at boot.
Verify
Occurrences appear for three consecutive days with nobody logged in - have readythe printer, plus consumables
Why Label size, material and the encoder's margin settings decide whether the system works in the field, and none of it is testable on a screen.
Get it Any thermal label printer and the sticker stock that survives your environment.
Verify
A production-size label scans on a cheap phone in poor light - API keyfree for a public topic; small if self-hosted
Why Assignment and overdue alerts go through it, and its failures need to be visible rather than assumed.
Get it A self-hosted or public push service with a topic URL you control.
Verify
A test alert arrives and a deliberately broken URL appears in the failure log - decidefree
Why The two produce visibly different schedules after the first late job, and deciding per plan later means deciding inconsistently across a hundred assets.
Get it Write the rule down, for example calendar inspections anchored to the due date and lubrication anchored to completion.
Verify
The rule is in the README and each seeded plan matches it
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- locations(id, name, parent_id) and assets(id, code, name, location_id, parent_asset_id, status, installed_on, notes) - assets.code is assigned once, printed on the label, and never reused even after the asset is scrapped - meters(id, asset_id, name, unit, kind) · kind is cumulative or resettable, which changes what a lower reading means - readings(id, meter_id, at, value, taken_by, source, offset_applied) · insert only, and a decrease is an event rather than a correction - pm_plans(id, asset_id, title, mode, interval_days, meter_id, interval_units, anchor, lead_days, checklist_json) · anchor is due_date or completion - pm_occurrences(id, plan_id, due_at, generated_at, generated_from_reading_id, work_order_id, skipped_count) · unique on plan_id and due_at - work_orders(id, code, asset_id, plan_id, priority, assignee_id, due_at, status, opened_at, closed_at) - work_log(id, work_order_id, at, actor_id, kind, text, minutes, parts_json, photo_path) · append only, and this is the history
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/maintenance.db | Assets, work orders and the whole work history. Back up with the photographs. |
PHOTO_DIR | required | ./data/photos | Work order attachments. Restored together with the database or not at all. |
SESSION_SECRETsecret | required | hex-64-chars-redacted | Signs session cookies. Rotating it logs everyone out. |
NTFY_URLsecret | required | https://ntfy.example.com/maintenance | Push endpoint for assignment and overdue alerts. Failures appear in the admin log. |
PM_LOOKAHEAD_DAYS | required | 14 | How far ahead occurrences are generated. Longer means a fuller queue, not more work. |
PM_CATCHUP_MAX | required | 1 | Occurrences created after an outage. Above one and you get an avalanche. |
MAX_UPLOAD_BYTES | required | 8388608 | Per photograph, checked before anything is written to disk. |
BACKUP_DIR | required | /var/backups/maintenance | Database and photograph archive pairs. Keep generations. |
The build, in order
Assets, codes and a label that scans in a plant room
Every asset has a code assigned once, a printable label, and a page that opens from scanning it.
An import must not renumber what is stuck to a machine.
Files
src/assets/codes.jstests/codes.test.jsLabels get scratched and phones get left in the van.
Files
src/views/label.ejsThe only test that predicts anything about the plant room.
Files
docs/label-test.md
done when · tick each as it passeswatch out- Deriving the label from the database identifier means the first data import renumbers every asset and every label already stuck to a machine now points at the wrong one, which nobody discovers until two technicians file work against the same pump. Assign a code once, never reuse it, and keep it independent of any row identifier.
- Encoder libraries differ on whether they emit the clear margin the symbol specification requires, and a label printed edge to edge on a sticker fails to scan for reasons that look like bad lighting. Read your encoder's documentation on the quiet zone, print a real label, and scan it through a smear of grease before you print two hundred.
Work a technician can complete with one thumb
The field views are large, fast, and safe to tap twice on a bad connection.
Gloves and a dropdown do not mix.
Files
src/views/queue.ejsThe append-only log cannot be tidied up later.
Files
src/middleware/idempotency.jstests/idempotency.test.jsEvidence collected later is evidence not collected.
Files
src/work/photos.js
done when · tick each as it passeswatch out- A dropdown and a save button, on a phone, in gloves, means the work gets written up at the end of the shift from memory or not at all, and a maintenance system nobody updates in the field is a spreadsheet with extra steps. Put the three transitions people actually use on buttons big enough to hit without looking.
- A flaky connection turns one status change into two identical log entries, and because the log is append-only you cannot tidy it up afterwards. Issue a token with the form, key the write on it, and make the second submission return the first result.
Recurrence that neither duplicates nor avalanches
Preventive plans generate exactly one occurrence per due date, and a scheduler outage does not produce a month of overdue work.
Idempotence by constraint, not by hoping the job runs once.
Files
src/pm/generate.jssrc/db/schema.sqlThe two behaviours are genuinely different requirements.
Files
tests/pm-anchor.test.jsForty overdue jobs is the same as no overdue jobs.
Files
src/pm/catchup.js
done when · tick each as it passeswatch out- Creating the next occurrence inside the request that closes a work order means a scheduler that restarts mid-run, or two technicians closing at once, produces duplicate jobs, and duplicates in a maintenance queue destroy trust in the queue faster than anything else. Generate outside the request, key on plan and due date, and let a re-run be a no-op by construction.
- Computing every occurrence that was missed while the scheduler was down gives you forty overdue jobs on Monday morning, nobody does them, and the overdue count stops meaning anything. Create the next one, record the skipped count on it, and show that count on the work order so the gap is visible rather than pretended away.
Meters, and the day a reading goes backwards
Meter-based plans work from irregular readings and survive a replaced or rolled-over gauge.
A cached total is a corruption waiting for a bad reading.
Files
src/meters/interval.jsGauges get replaced and counters roll over.
Files
src/meters/events.jstests/meter-rollover.test.jsDeleting the evidence is how history stops being trustworthy.
Files
src/meters/readings.js
done when · tick each as it passeswatch out- A run-hours gauge that is replaced reads from zero, and a plan that subtracts the last reading from the current one sees a large negative interval, decides the service is overdue by thousands of hours, and generates work forever. Treat a decrease on a cumulative meter as an event, not a data entry error, pause the plans, and store an offset once a human has confirmed what happened.
- Maintaining a running total in the asset row means one bad reading corrupts every future calculation and there is no way back. Compute from the readings table every time, and keep the readings even when one is later marked as mistaken.
Roles, backup and a history worth havingproduct builder
Access is separated, the database and photographs are backed up together, and a restore is exercised rather than described.
A hidden link is not access control.
Files
tests/permissions.test.jssrc/auth/roles.jsEither alone restores to a system full of missing evidence.
Files
scripts/backup.shtests/restore.test.jsThe history is only worth keeping if it cannot be rewritten.
Files
src/work/corrections.js
done when · tick each as it passeswatch out- Copying the database file while the application is running produces a file that opens, passes a quick look, and is missing the most recent transactions, which is precisely the work you most wanted to keep. Use the database's own backup command, take the photograph directory in the same run, and assert the pair restores together in the suite.
- Letting an administrator edit a work log entry to fix a typo turns the asset history into an opinion, and the whole reason to keep ten years of it is that nobody can change it. Append a correction row that references the original instead, and render corrections next to what they correct.
That is the whole plan for MaintenEase. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Predictive maintenance from sensor data.
- Accounting and purchasing integrations.
- Multi-site tenancy and single sign-on.
- A native mobile application.
- managed backups and hosted reliability
- polished mobile field workflows
- advanced analytics and predictive features
- integrations, API, and SSO
- onboarding and data-import support
- Parts and stock levels, which are a second inventory problem and deserve their own uniqueness thinking
- An offline queue for the field views, which is worth doing once you know which three actions actually matter
- Compliance reporting on preventive completion, computed from occurrences and closures already recorded
Need the files? The project pack on the verdict page hands your agent the whole brief · more tasks.