← THYNKTA
PROJECT

MaintenX

Maintenance for palm oil mills. Every machine keeps its own chart — what has gone wrong with this one, how often, and who fixed it.

PWA · 20 USERS · 112 ASSETS · LIVE
Screw Press P15 No.3
K2-PRS-16
CRITICAL
7Failures
31.5 hTotal downtime
4.5 hAvg repair
38 dBetween failures
Worn press coneClosed
M01-2026-0184
Mechanical ZAM · KIMI
Main shaft bearing seizedClosed
M01-2026-0121
Mechanical SYUKOR
OrdersVerifyHistoryAssets

ONE MACHINE'S RECORD

The question it answers

A mill is a hundred machines that all run at once. Every one will break. That was never the question. What costs you is what comes after: what broke last time, and what did we do about it?

Before this, that answer sat in a logbook, a WhatsApp thread, or somebody's memory. None of it searchable.

How a fault moves
The part that matters

A chart, not a work log

My first attempt recorded work: log the fault, assign it, close it. A ticketing system with a maintenance vocabulary. That was wrong, and it took a while to see why.

Recording work tells you what your people did. It tells you nothing about your machines — and the machine is what stops production.

Every machine should have a chart. You open it and see what has gone wrong with this one, how often, and what was done — like a doctor pulling a patient's history before deciding anything.

Once you have that, something useful falls out for free. A screw press that used to fail every 38 days starts failing every 20. No single work order looks alarming. But the chart says it plainly, months before the machine strands you mid-season.

Decisions from the mill, not the code

Electrical work reaches a chargeman

That is competency, not preference. So when a fault is tagged electrical, only certified chargemen appear in the assignment list. The mistake stops being possible rather than being caught afterwards.

Two people on a hot job

We work in pairs for safety. A job can be assigned to two, and when it is assigned to one, that shows. Not to police anybody — but if you are recording who did the work anyway, you may as well be able to answer whether the buddy system was followed.

Nobody closes their own work

A technician submits; someone else verifies. That single separation is what makes the history worth trusting a year later.

Malay on the floor, English in the office

The alternative is a system half the crew quietly works around.

In the rules

None of that is a screen validation. It sits in the database rules, where the app cannot talk its way past it — and neither can I.

firestore.rules workOrders · excerpt
function was() { return resource.data.status; }
function now_() { return request.resource.data.status; }
function mine_() { return request.auth.uid in resource.data.assignees; }

function mayAssign() {
  return myRole() == 'admin'
         || (myRole() == 'foreman' && me().trade == resource.data.faultType);
}

function mayVerify() {
  return myRole() == 'admin' || myRole() == 'foreman' || myRole() == 'reporter';
}

function historySealed() {
  return frozen('code') && frozen('reportedBy') && frozen('reportedAt')
         && frozen('assetId') && frozen('shift') && frozen('faultType');
}

allow update: if activeUser()
              && historySealed()
              && tenancyFrozen()
              && frozen('downtimeMinutes') && frozen('soloJob')
              && (

  (mayAssign()
   && was() in ['REPORTED', 'ASSIGNED', 'IN_PROGRESS']
   && now_() in ['ASSIGNED', 'REPORTED']
   && request.resource.data.assignees.size() ==
      request.resource.data.assigneeNames.size()
   && (now_() == 'REPORTED' || selfStamp('assignedBy')))

  || (myRole() == 'technician' && mine_()
      && was() in ['ASSIGNED', 'IN_PROGRESS']
      && now_() in ['IN_PROGRESS', 'PENDING_VERIFICATION']
      && frozen('assignees')
      && (now_() == 'IN_PROGRESS'
          || (selfStamp('closedBy')
              && request.resource.data.closedAt == request.time)))

  || (mayVerify() && was() == 'PENDING_VERIFICATION' && now_() == 'CLOSED'
      && selfStamp('verifiedBy')
      && request.resource.data.verifiedAt == request.time)

  || (myRole() == 'admin' && now_() == 'CANCELLED'
      && was() != 'CLOSED' && was() != 'CANCELLED'
      && selfStamp('cancelledBy')
      && request.resource.data.cancelReason is string
      && request.resource.data.cancelReason.size() > 0)
);

THE WORK ORDER STATE MACHINE, AS ENFORCED

Where it runs
USERS 20 ASSETS 112 ROLE TIERS 4 STACK PWA · Firebase LANGUAGES BM · EN

Live at one Malaysian mill. No app store, no hardware, no IT project — it runs on the phones the crew already carry, and everyone signs in once.

Contact

If you run a mill and this sounds like your problem — akmalkharolanuar@gmail.com