Small retailers with EAS pedestals at the door often have no record of when the alarm goes off or why. Staff reset it, check the customer's bag, and move on. Without a log, nobody can tell whether alarms come from real theft attempts, tags missed at checkout, or a pedestal that needs servicing.
Here is a weekend-sized project: a single HTML file that runs on the shop's tablet, logs each alarm with a reason, stores everything in localStorage, and exports a CSV. No server, no accounts, no dependencies.
What we want to capture
Each alarm event gets:
- a timestamp
- a reason:
missed-tag,tag-from-other-store,no-item-found,suspected-theft, orother - an optional note
That is enough to spot patterns later.
The HTML
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Door Alarm Log</title>
<h1>Door alarm log</h1>
<div id="buttons"></div>
<input id="note" placeholder="Optional note">
<button id="export">Export CSV</button>
<table id="log"></table>
<script src="alarm-log.js"></script>
</html>
Big buttons matter here. Whoever responds to the alarm should be able to log it in one tap.
The JavaScript
const REASONS = ["missed-tag", "tag-from-other-store", "no-item-found", "suspected-theft", "other"];
const KEY = "doorAlarmLog";
const load = () => JSON.parse(localStorage.getItem(KEY) || "[]");
const save = (rows) => localStorage.setItem(KEY, JSON.stringify(rows));
function addEvent(reason) {
const rows = load();
rows.push({ time: new Date().toISOString(), reason, note: note.value.trim() });
save(rows);
note.value = "";
render();
}
function render() {
const rows = load().slice(-20).reverse();
log.innerHTML = rows.map(r =>
`<tr><td>${new Date(r.time).toLocaleString()}</td><td>${r.reason}</td><td>${r.note}</td></tr>`
).join("");
}
function exportCsv() {
const rows = load();
const esc = (s) => `"${String(s).replace(/"/g, '""')}"`;
const csv = ["time,reason,note", ...rows.map(r => [r.time, r.reason, r.note].map(esc).join(","))].join("\n");
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = `door-alarms-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
}
REASONS.forEach(r => {
const b = document.createElement("button");
b.textContent = r;
b.onclick = () => addEvent(r);
buttons.appendChild(b);
});
document.getElementById("export").onclick = exportCsv;
render();
Note the esc() helper: notes typed by staff can contain commas and quotes, so every field is quoted for the CSV.
A weekly summary in one function
function weeklySummary() {
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
return load()
.filter(r => Date.parse(r.time) >= weekAgo)
.reduce((acc, r) => ({ ...acc, [r.reason]: (acc[r.reason] || 0) + 1 }), {});
}
Log it to the console or render it at the top of the page.
Reading the results
The code is the easy part. The value is in what the numbers suggest:
-
Lots of
missed-tag: a checkout training or tag-removal problem, not a theft problem. -
Lots of
tag-from-other-store: shoppers are carrying items from neighbors whose tags still trigger your system. Signage and a polite process help. -
Lots of
no-item-found: the pedestals may need adjusting or servicing. - Clusters at certain hours: look at staffing during those windows.
If you are new to the retail side of this, the loss prevention articles on the Qova Security blog explain the store-level practices behind each of these alarm causes.
Caveats
localStorage lives in one browser on one device, so export regularly and keep the CSVs somewhere backed up. Clearing the browser data wipes the log. If you need multiple devices, the next step is a small backend or a shared spreadsheet, but for one door and one tablet this is plenty.
Published by Qova Security.
Top comments (0)