DEV Community

Qova Security
Qova Security

Posted on

Build a Tiny Browser Logger for Store Door Alarms (Vanilla JS, No Backend)

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, or other
  • 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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 }), {});
}
Enter fullscreen mode Exit fullscreen mode

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)