Pick an activity, then scan each student — a USB scanner works anywhere on this page. Scan a point-value QR to switch what you're scoring.
Tap a name to give points
Scan history
The full record of every scan — not just the last few shown on the Dashboard's Recent scans.
Leader Board
Live point totals. Great for the projector — filter by class, group, activity, or time period.
Raffle
Spin the wheel to pick a winner. Each slice is sized to that student's tickets — the more tickets, the bigger the slice and the better the odds.
Who's in this draw
Untick a boy to keep him out of the next draw. Nothing is deducted and no points change — he simply isn't on the wheel. ↺ Un-remove everyone above puts them all back.
Recent winners
Chinese Auction
Set up your prizes, then let each student place tickets on the prizes he wants. Each prize is drawn from its own pile — the more tickets a student puts in, the better his odds. With Each student's points, each prize has its own cost per ticket (deducted right away); removing a ticket refunds it — until that prize is drawn, after which it locks.
Prizes
Review
Pick a sub-tab to teach, test, or review your text.
Parsha & range
Shorashim
Practice the roots from the parsha. Pick a mode, then play.
Word bank — tap + to add
Common shorashim from the parsha. Tap one to add it to your list (or add them all).
The checkbox marks a shoresh as "learned" — only checked ones show up in the games.
Students
Add your students once. Each gets a QR code you can print.
Add many at once
One name per line. To set a group, write Name, Group.
Class lists from your Google Sheet
Keep your rosters in the same sheet you use for syncing. Add a tab named Roster with columns — Name, Class, and optionally Photo URL (a direct, publicly-viewable image link) — then pull them in here. Each class becomes selectable from the dropdown in the top-right corner. Photo URLs only fill in for students who don't already have a photo set here.
First-time setup (about 2 minutes)
- Set up the Google Sheet link once on the Sync to Google Sheets tab (if you haven't already).
- Make sure you've pasted the latest Code 1 there and re-deployed — it now includes the roster reader.
- In Apps Script, pick
setupRosterin the function dropdown and click Run once. That creates a Roster tab with the Name / Class headers and a few example rows. - Open your sheet's Roster tab and fill in your students — one per row, with their class in the second column. List as many classes as you like.
- Come back here and tap Import roster from sheet. Matching names keep their points; new names get added.
Paste a list (easiest — no Apps Script needed)
One student per line as Name, Class — or just select the Name and Class columns in your Google Sheet, copy, and paste here (a header row is skipped automatically).
Activities & point values
These are the buttons on the Scan screen. Change a point value right here — tap the +/– or type a number. Use a negative value for a deduction.
Seating chart & print
This is the one page you need to start scanning. Arrange your room here, then print it. Every student's code sitting in his own seat, with the codes for your activities underneath. Print it, keep it on your desk or clipboard, and scan an activity code then a student — that's a whole scoring action in two scans.
Preview — this is what will print, and you edit it here. Drag a boy into a different desk, or ✕ to empty his seat. Drag a code to re-order it or move it to another section, ✕ to take it off, and click a section's name to rename it. Anything you take off waits in the tray below the sheet.
Print: Students
Each student's own QR code, and their own full page of one-scan-does-it-all codes for every activity.
Student labels
Personal scoring pages (one per student)
Each student gets their own page with a QR code for every activity, already linked to them — scan one code and it awards that activity to that student in a single scan, no separate student scan needed. The Custom card uses whatever amount is in the Custom box on the Scan tab at the moment it's scanned, not the amount at print time.
Print: Activities
Print: Points
Prints one card per student with their current points — uses whatever class/group/activity/time filter is currently set on the Leader Board tab.
Backup & Sheets
Your scores save automatically on this computer. Back up regularly so nothing is ever lost — and optionally sync to your own Google Sheet below for a permanent, view-anywhere record.
☁ Save everything to your Google Sheet
Pushes a full snapshot of your data — students, activities, scores, settings, passes, pesukim, everything — to a "Snapshot" tab on your connected Google Sheet. This also happens automatically in the background every 30 seconds.
☁ Restore everything from your Google Sheet
Lost your data? Switched devices? Pull back the latest snapshot saved on your Google Sheet. This replaces everything currently here.
Back up everything
Save a file with your full roster, activities, and scores. Keep it somewhere safe.
Install Menchmark as an app Best way to work offline
Install Menchmark from your browser (look for Install in the menu, or the ⊕ / install icon in the address bar) to run it like a real app — its own window, an icon on your device, and persistent storage that resists the browser clearing your data. Works fully offline once installed, and it stays the same Menchmark — the same students, the same scores, whether you are online or not.
Installing helps a lot, but on locked-down or shared school Chromebooks an administrator can still clear data on sign-out — so keep your Google Sheet backup connected as your ultimate safety net.
Download a separate copy — for a computer with no internet at all
Saves a single HTML file with your current students, activities, and scores already loaded in — plus the QR scanner built right in. For a classroom computer that never goes online, or a spare machine you want a self-contained copy on. Needs internet once, to prepare the file; after that, just open it in any browser like any other file.
The downloaded copy keeps its own separate data. Points you enter in it stay in it — they will not appear here, and points entered here will not appear there.
If you just want Menchmark to work without internet, install it as an app above instead — that keeps one set of data. Use this download only when you need a copy that lives entirely on its own.
Restore from a backup
Load a backup file. This replaces everything currently here.
Restore from a Log CSV
No backup file? If you synced to Google Sheets, download the Log tab as CSV (File → Download → .csv) and load it here. Rebuilds your students, current scores, and full points history from the scan log — activities, students, and points are all reconstructed.
Export standings
A spreadsheet (CSV) of every student and their current score.
Reset spendable points
Zeroes current point balances (e.g. to start a fresh week) for the current class or one group. Lifetime Trends history is kept.
Start a new period
Reset all scores to zero but keep your students and activities.
Clear ALL data
Wipes everything — institution & class name, students, activities, scores, scan log, raffle history, and the saved Google Sheet link. Use this to start completely fresh (a new class, a new year). Download a backup first if you're not sure.
Sync to Google Sheets
Send every scan and the live standings to your own Google Sheet, so you can see totals anywhere and keep a permanent record.
First-time setup — do this once (about 5 minutes)
- Open sheets.google.com and make a new blank sheet. Name it (e.g. "Sample Class 1 Points").
⚠️ Every teacher needs their own sheet. Do not paste another teacher's link here — your scans, scores, and backups would mix together and overwrite each other. Each teacher creates one sheet and gets their own link. - In that sheet's menu: Extensions → Apps Script.
- At the top-left, click Untitled project and rename it (e.g. "Sample Class 1 Points") so the permission screen later looks familiar, not anonymous.
- Delete any code in the editor and paste in Code 1 below.
- Click the gear (Project Settings) on the left and tick "Show appsscript.json manifest file in editor."
- Back in the editor (<> Editor), open the file named appsscript.json, delete what's there, and paste in Code 2. This limits the app to this one sheet only.
- Click Deploy → New deployment. Type: Web app. Set Execute as: Me and Who has access: Anyone. Click Deploy.
- Google will warn the app "isn't verified" — that's normal for your own script. Click Advanced → Go to … (unsafe) → Allow. (It runs as you, on your sheet only.)
- Copy the Web app URL it gives you (ends in
/exec), paste it in the box above, and click Save link.
Tip: to test by hand, pick testSetup in the function dropdown and click Run — that authorizes the script and drops one test row in your Log. Don't click Run on doPost; it only works from a real web request and will show a "postData" error otherwise.
Code.gs
Already set up? Two steps, not one:
- Re-copy this updated Code 1 and paste it over your existing
Code.gs(replacing it entirely), then save (Ctrl/Cmd+S). - Redeploy it — pasting new code alone does not update your live sync link. In Apps Script: Deploy → Manage deployments, click the ✏️ pencil icon next to your existing deployment, set Version to New version, then click Deploy. (Do not click "New deployment" — that would give you a different link and break your existing sync URL in this app.)
function doGet(e) {
if (e && e.parameter && e.parameter.action === "roster") {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Roster");
if (!sh) {
var all = ss.getSheets();
for (var i = 0; i < all.length; i++) {
var nm = all[i].getName().toLowerCase().replace(/\s+/g, "");
if (nm === "roster" || nm === "roaster" || nm === "classlist" ||
nm === "classes" || nm === "students" || nm === "kids") { sh = all[i]; break; }
}
}
var out = [];
if (sh && sh.getLastRow() > 1) {
var rows = sh.getRange(2, 1, sh.getLastRow() - 1, 3).getValues();
rows.forEach(function (r) {
var name = (r[0] || "").toString().trim();
var cls = (r[1] || "").toString().trim();
var photo = (r[2] || "").toString().trim();
if (name) out.push({ name: name, cls: cls, photo: photo });
});
}
return ContentService.createTextOutput(JSON.stringify(out))
.setMimeType(ContentService.MimeType.JSON);
}
// --- Snapshot pull (Phase 1) ---
if (e && e.parameter && e.parameter.action === "snapshot") {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Snapshot");
if (!sh || sh.getLastRow() < 2) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, error: "No snapshots found." }))
.setMimeType(ContentService.MimeType.JSON);
}
// Optional: pull snapshot for a specific device
var deviceId = (e.parameter.deviceId || "").trim();
var lastRow = sh.getLastRow();
var lastCol = sh.getLastColumn();
// Walk backward from the bottom to find the newest matching row
for (var r = lastRow; r >= 2; r--) {
var row = sh.getRange(r, 1, 1, lastCol).getValues()[0];
var rowDevice = (row[1] || "").toString().trim();
if (deviceId && rowDevice !== deviceId) continue;
// Reassemble JSON from chunks (columns C onward)
var chunks = [];
for (var c = 2; c < row.length; c++) {
var chunk = (row[c] || "").toString();
if (chunk) chunks.push(chunk);
}
if (!chunks.length) continue;
var json = chunks.join("");
return ContentService.createTextOutput(JSON.stringify({ ok: true, deviceId: rowDevice, snapshot: json }))
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput(JSON.stringify({ ok: false, error: "No snapshot found" + (deviceId ? " for device " + deviceId : "") + "." }))
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput("Points sync is live.");
}
// Log columns are: Time, Student, Class/Group, Activity, Points, New total, ID,
// Undo Of, Note. Note is appended at the very end deliberately — sheets that
// were created before this column existed keep their original 8-column
// header untouched; new rows simply carry a 9th value. Add a "Note" label to
// I1 by hand on an older sheet if you'd like the column header to show it.
// The ID column is how duplicate scans get caught — if the app sends the same
// scan twice (a flaky connection, a manual resync), the second one is skipped.
function logHasId(log, id) {
if (!id) return false;
var last = log.getLastRow();
if (last < 2) return false;
var ids = log.getRange(2, 7, last - 1, 1).getValues();
for (var i = 0; i < ids.length; i++) { if (ids[i][0] === id) return true; }
return false;
}
// Sort helper for rebuildLeaderBoard — "First Last" becomes "Last, First"
// so ties on points sort alphabetically by last name, same as the app.
function lastFirstKey(name) {
var parts = (name || "").toString().trim().split(/\s+/);
if (parts.length < 2) return name || "";
return parts[parts.length - 1] + ", " + parts.slice(0, -1).join(" ");
}
// Recomputes a "Leader Board" tab straight from the Log, so standings are
// always current without anyone having to click "Send standings now."
function rebuildLeaderBoard() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName("Log");
if (!log || log.getLastRow() < 2) return;
var rows = log.getRange(2, 1, log.getLastRow() - 1, 6).getValues();
var totals = {};
rows.forEach(function (r) {
var name = r[1];
if (!name) return;
totals[name] = { group: r[2], total: r[5] }; // last row per student wins — rows are in time order
});
var list = Object.keys(totals).map(function (name) {
return { name: name, group: totals[name].group, total: totals[name].total };
});
list.sort(function (a, b) { return b.total - a.total || lastFirstKey(a.name).localeCompare(lastFirstKey(b.name)); });
var lb = ss.getSheetByName("Leader Board") || ss.insertSheet("Leader Board");
lb.clear();
lb.appendRow(["Rank", "Student", "Class/Group", "Points"]);
list.forEach(function (it, i) { lb.appendRow([i + 1, it.name, it.group, it.total]); });
lb.setFrozenRows(1);
}
// ---- Snapshot helpers (Phase 1) ----
// Google Sheets cells have a ~50,000 character limit. We chunk the JSON
// across columns C, D, E… so even large data objects (with big logs or
// photo data-URLs) are stored safely.
var SNAPSHOT_CHUNK_SIZE = 45000;
var SNAPSHOT_MAX_PER_DEVICE = 5;
function saveSnapshot(ss, deviceId, jsonString) {
var sh = ss.getSheetByName("Snapshot") || ss.insertSheet("Snapshot");
if (sh.getLastRow() === 0) {
sh.appendRow(["Time", "Device ID", "Data →"]);
sh.setFrozenRows(1);
}
// Build row: [timestamp, deviceId, chunk1, chunk2, …]
var row = [new Date(), deviceId];
for (var i = 0; i < jsonString.length; i += SNAPSHOT_CHUNK_SIZE) {
row.push(jsonString.substring(i, i + SNAPSHOT_CHUNK_SIZE));
}
sh.appendRow(row);
// Trim old snapshots for this device — keep only the newest N
trimSnapshots(sh, deviceId);
}
function trimSnapshots(sh, deviceId) {
var lastRow = sh.getLastRow();
if (lastRow < 2) return;
var deviceCol = sh.getRange(2, 2, lastRow - 1, 1).getValues();
var matchingRows = [];
for (var i = 0; i < deviceCol.length; i++) {
if ((deviceCol[i][0] || "").toString().trim() === deviceId) {
matchingRows.push(i + 2); // 1-indexed sheet row
}
}
// If this device has more than SNAPSHOT_MAX_PER_DEVICE snapshots, delete oldest
if (matchingRows.length > SNAPSHOT_MAX_PER_DEVICE) {
var toDelete = matchingRows.slice(0, matchingRows.length - SNAPSHOT_MAX_PER_DEVICE);
// Delete from bottom up so row numbers don't shift
for (var j = toDelete.length - 1; j >= 0; j--) {
sh.deleteRow(toDelete[j]);
}
}
}
// Returns a log sheet, creating it and writing its header EXACTLY ONCE.
//
// The header used to be written with a bare check-then-act:
// if (sh.getLastRow() === 0) sh.appendRow(header);
// One "Send Attendance" fires one request per student — twenty-odd at once —
// and Apps Script runs those concurrently. Two executions that both arrive
// before either has appended BOTH see an empty sheet, so both write a header.
// That is the doubled header. It is the ONLY thing this race produced: the
// data rows are one per request either way, which is why the header was the
// one symptom that survived a clean-sheet test.
//
// The lock closes the window. flush() is not optional — it forces the header
// to be visible to the next execution before the lock is released; without it
// the append can still be pending and the next execution reads 0 again.
function logSheet_(ss, name, header) {
var lock = LockService.getScriptLock();
var held = false;
try { held = lock.tryLock(30000); } catch (err) {}
try {
var sh = ss.getSheetByName(name) || ss.insertSheet(name);
if (sh.getLastRow() === 0) {
sh.appendRow(header);
SpreadsheetApp.flush();
}
return sh;
} finally {
if (held) { try { lock.releaseLock(); } catch (err) {} }
}
}
function doPost(e) {
if (!e || !e.postData) {
return ContentService.createTextOutput(
"No data received. This runs from the Points app. To test by hand, run testSetup() instead."
);
}
var ss = SpreadsheetApp.getActiveSpreadsheet();
var body = JSON.parse(e.postData.contents);
// --- Snapshot save (Phase 1) ---
if (body.type === "snapshot") {
var deviceId = (body.deviceId || "unknown").toString().trim();
var snapshot = body.snapshot || "{}";
// snapshot is the full JSON string of the app's data object
saveSnapshot(ss, deviceId, snapshot);
return ContentService.createTextOutput("ok");
}
if (body.type === "scan" || body.type === "correction") {
var log = logSheet_(ss, "Log", ["Time", "Student", "Class/Group", "Activity", "Points", "New total", "ID", "Undo Of", "Note"]);
if (!logHasId(log, body.id)) {
log.appendRow([new Date(body.ts), body.name, body.group, body.label, body.delta, body.total, body.id || "", body.undoOf || "", body.note || ""]);
rebuildLeaderBoard();
}
}
// A rebbi tagged an existing entry with a note (e.g. "talked to Dovid")
// after the fact, from the app's History tab. Finds that scan's row by its
// ID (column G) and fills in column I (Note) — added at the END of the row
// so older, already-populated sheets are never shifted or reflowed.
if (body.type === "note") {
var nlog = ss.getSheetByName("Log");
if (nlog && body.id) {
var nlast = nlog.getLastRow();
if (nlast >= 2) {
var nids = nlog.getRange(2, 7, nlast - 1, 1).getValues(); // column G = ID
for (var ni = 0; ni < nids.length; ni++) {
if (nids[ni][0] === body.id) {
nlog.getRange(ni + 2, 9).setValue(body.note || ""); // column I = Note
break;
}
}
}
}
}
if (body.type === "standings") {
var st = ss.getSheetByName("Standings") || ss.insertSheet("Standings");
st.clear();
st.appendRow(["Rank", "Student", "Class/Group", "Points"]);
(body.rows || []).forEach(function (r) { st.appendRow(r); });
}
if (body.type === "pasuk") {
var pl = logSheet_(ss, "Pesukim Log", ["Time", "Student", "Class/Group", "Pasuk"]);
pl.appendRow([new Date(body.ts), body.name, body.group, body.ref + ":" + body.pasuk]);
}
if (body.type === "pasukgrid") {
var pc = ss.getSheetByName("Pesukim Chart") || ss.insertSheet("Pesukim Chart");
pc.clear();
var header = ["Student", "Class/Group"];
for (var v = body.from; v <= body.to; v++) { header.push(body.ref + ":" + v); }
header.push("Done");
pc.appendRow(header);
(body.rows || []).forEach(function (r) { pc.appendRow(r); });
}
if (body.type === "totals") {
var st = ss.getSheetByName("Student Totals") || ss.insertSheet("Student Totals");
st.clear();
st.appendRow(["Updated", new Date()]);
st.appendRow(body.header || []);
(body.rows || []).forEach(function (r) { st.appendRow(r); });
st.setFrozenRows(2);
}
// Attendance. Takes a BATCH — {rows:[{ts,date,name,group,status}, ...]} — and
// writes the whole class in ONE locked operation.
//
// Why: the app used to send one request per student. Apps Script runs those
// concurrently, each execution called getLastRow() to decide where to append,
// and several read the same row number before any had written — so they
// appended over each other. A class of 20 landed 7 rows with wrong names.
// logSheet_'s lock only ever guarded the HEADER write; the appends were
// unprotected. Taking the lock around the whole append is the fix, and
// batching is what makes one lock enough.
//
// BOTH SHAPES ARE ACCEPTED, and this is not only for the transition. Any
// rebbi who has not redeployed is still sending one-at-a-time, and a script
// that understood only the new shape would silently drop his attendance
// entirely. The single-student auto-push that fires when a status changes on
// the Attendance tab also still uses the old shape by design — it is one row
// by nature. Do not remove this branch.
if (body.type === "attendance") {
var attRows = Array.isArray(body.rows)
? body.rows
: [{ ts: body.ts, date: body.date, name: body.name, group: body.group, status: body.status }];
var attLock = LockService.getScriptLock();
var attHeld = false;
try { attHeld = attLock.tryLock(30000); } catch (err) {}
try {
var att = logSheet_(ss, "Attendance Log", ["Time", "Date", "Student", "Class/Group", "Status"]);
// setValues() in one call rather than appendRow() per row: one write, one
// getLastRow() read, and nothing else can interleave between them.
var attOut = [];
for (var ai = 0; ai < attRows.length; ai++) {
var ar = attRows[ai] || {};
// SKIP EMPTY ROWS. A send with nothing to send used to POST anyway with
// no body.rows at all, so the single-row fallback above built
// [new Date(undefined), undefined, undefined, undefined, undefined] and
// wrote a garbage row dated 1969 (new Date(undefined) is Invalid Date,
// which lands at the epoch once the Sheet coerces it).
// The app no longer POSTs an empty diff, but the guard belongs here too:
// this script has to keep serving older app versions that still do, and
// an empty row is never something anyone meant to record.
if (!ar.name && !ar.status) continue;
attOut.push([new Date(ar.ts), ar.date, ar.name, ar.group, ar.status]);
}
if (attOut.length) {
att.getRange(att.getLastRow() + 1, 1, attOut.length, 5).setValues(attOut);
SpreadsheetApp.flush();
}
} finally {
if (attHeld) { try { attLock.releaseLock(); } catch (err) {} }
}
}
if (body.type === "tracker") {
var trk = logSheet_(ss, "Tracker Log", ["Time", "Student", "Class/Group", "Activity"]);
trk.appendRow([new Date(body.ts), body.name, body.group, body.activity]);
}
if (body.type === "hw") {
var hwl = logSheet_(ss, "Homework Log", ["Time", "Date", "Student", "Class/Group", "Checked"]);
hwl.appendRow([new Date(body.ts), body.date, body.name, body.group, body.checked ? "Yes" : "No"]);
}
return ContentService.createTextOutput("ok");
}
// Run THIS one by hand (pick it in the function dropdown, click Run) to
// authorize the script and confirm it works. It writes one test row you
// can delete afterward. Do not click Run on doPost — it needs a web request.
function testSetup() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName("Log") || ss.insertSheet("Log");
if (log.getLastRow() === 0) {
log.appendRow(["Time", "Student", "Class/Group", "Activity", "Points", "New total", "ID", "Undo Of", "Note"]);
}
log.appendRow([new Date(), "Test student", "Gimel 1", "Setup test", 0, 0, "test-" + Date.now(), "", ""]);
rebuildLeaderBoard();
}
// Run THIS once to create the "Roster" tab with Name / Class headers and a
// few example rows. Then fill it in with your students and their classes, and
// tap "Import roster from sheet" on the Students tab in the app.
function setupRoster() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Roster") || ss.insertSheet("Roster");
if (sh.getLastRow() === 0) {
sh.appendRow(["Name", "Class"]);
sh.appendRow(["Yaakov Cohen", "Math 5A"]);
sh.appendRow(["Dovid Levi", "Math 5A"]);
sh.appendRow(["Moshe Stern", "Math 6B"]);
}
}
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly"
]
}
With Code 2 in place, the permission screen will say it only wants the spreadsheet "this application is installed in" — meaning your points sheet, nothing else in your Drive.
If you ever change the code, you must re-deploy for it to take effect: Deploy → Manage deployments → ✏️ Edit → Version: New version → Deploy. The URL stays the same.
Already set up from before? The Pesukim chart needs the newest Code 1 above. Re-paste it, then re-deploy: Deploy → Manage deployments → ✏️ Edit → Version: New version → Deploy. Your link stays the same.
Updating from an older setup: the newest Code 1 adds an ID and Undo Of column at the end of the Log tab (used to stop the same scan being recorded twice) and a new Leader Board tab that recalculates itself from the Log automatically. Existing rows in Log are untouched; new columns just start filling in going forward. Re-paste and re-deploy as above.
Heads up: the page sends data without waiting for a reply, so a wrong link won't show an error here. After saving, do one scan and check that a row appears in your sheet's Log tab — if it does, you're set.
After-Snack Brachos
Quick access after snack — Al Hamichya for the 5 grains, with Borei Nefashos right below.
Al Hamichya · 5 grains
בָּרוּךְ אַתָּה יְיָ, אֱלֹהֵינוּ מֶלֶךְ הָעוֹלָם, עַל הַמִּחְיָה וְעַל הַכַּלְכָּלָה, וְעַל תְּנוּבַת הַשָּׂדֶה, וְעַל אֶרֶץ חֶמְדָּה טוֹבָה וּרְחָבָה שֶׁרָצִיתָ וְהִנְחַלְתָּ לַאֲבוֹתֵינוּ לֶאֱכוֹל מִפִּרְיָהּ וְלִשְׂבּוֹעַ מִטּוּבָהּ. רַחֵם (נָא) יְיָ אֱלֹהֵינוּ עַל יִשְׂרָאֵל עַמֶּךָ, וְעַל יְרוּשָׁלַיִם עִירֶךָ, וְעַל צִיּוֹן מִשְׁכַּן כְּבוֹדֶךָ, וְעַל מִזְבְּחֶךָ, וְעַל הֵיכָלֶךָ. וּבְנֵה יְרוּשָׁלַיִם עִיר הַקֹּדֶשׁ בִּמְהֵרָה בְיָמֵינוּ, וְהַעֲלֵנוּ לְתוֹכָהּ, וְשַׂמְּחֵנוּ בְּבִנְיָנָהּ, וְנֹאכַל מִפִּרְיָהּ, וְנִשְׂבַּע מִטּוּבָהּ, וּנְבָרֶכְךָ עָלֶיהָ בִּקְדֻשָּׁה וּבְטָהֳרָה.
כִּי אַתָּה יְיָ טוֹב וּמֵטִיב לַכֹּל, וְנוֹדֶה לְּךָ עַל הָאָרֶץ וְעַל הַמִּחְיָה. בָּרוּךְ אַתָּה יְיָ, עַל הָאָרֶץ וְעַל הַמִּחְיָה.
Borei Nefashos
בָּרוּךְ אַתָּה יְיָ אֱלֹהֵינוּ מֶלֶךְ הָעוֹלָם, בּוֹרֵא נְפָשׁוֹת רַבּוֹת וְחֶסְרוֹנָן, עַל כָּל מַה שֶּׁבָּרָאתָ לְהַחֲיוֹת בָּהֶם נֶפֶשׁ כָּל חָי. בָּרוּךְ חֵי הָעוֹלָמִים.
Trends
Lifetime record of everything earned — spending tickets in the Auction or Store never touches this history. Weeks run Monday–Friday. Numbers show how many times each behavior was recorded that week; arrows compare points to the week before.
Contest
Run a contest for as long as you like. While it runs, the points you scan are held on the contest instead of going into each boy’s score. When you finish, you choose — in two separate steps — whether to add them to everyone’s real score or let them go.
🏆 No contest running right now.
Scanning works exactly as it always does until you start one.
Prize Store
Students spend their points to buy prizes. Set up prizes and their point cost, then pick a student (or scan) and tap what they want.
Prizes for sale
Recent purchases
Bathroom Passes
Each student gets 1 pass per week. Scan a student's QR code when they use one. Reset the passes at the start of each week.
Attendance
Tracker
Track non-point activities — like how many times a student was called on. Each tap or scan logs one more, with a timestamp.
Homework
Scan or tap a student to mark their homework checked for the day. Time-stamped and synced to your Google Sheet if connected.
Gradebook
One grid for everything tracked: students down the side, tracked items across the top. Columns come straight from your tracked items — add an item and it shows up here on its own. Pick which items to show and over what dates, then print exactly what you see.
Spinner
Spin your current class roster, or switch to a custom list for anything else (activities, prizes, "who goes first"). Separate from the student Picker in the top corner.
Pulls live from whichever class is selected in the top-right corner. Check or uncheck names to include/exclude them from the spin.
Settings
Set-and-forget options that apply across the app. They save automatically on this computer.
Barcode/QR Scanner — connect directly (Serial/COM)
Most cheap USB scanners need no setup — they act like a keyboard, and just work on the Scan tab already. Use this only if your scanner connects as an actual serial/COM port device instead (common with some industrial, handheld, or Bluetooth-SPP scanners) — connecting it here reads scans straight from the port, everywhere in the app, instead of through the keyboard.
Tabs shown
Hide sections you don't use to keep the top of the app simpler. Setup and Settings always stay reachable so you can turn things back on.
Sound
🎤 Voice notes
Say a note onto the scan you just made instead of typing it. Scan a student, then press 🎤 Note next to Undo — or scan the printed 🎤 Voice Note card from wherever you're standing — and say what he did. Nothing is shown on screen while you speak, so nothing reaches the class; you read and fix your notes afterwards on the History tab.
Off to begin with — tick it to switch it on. While it is off the 🎤 buttons are hidden, the printed card does nothing, and the microphone is never opened at all. No note you have already saved is changed or removed, and typing a note on the History tab keeps working either way. Needs Chrome or Edge and a connection — it uses your browser's own dictation, so there is nothing to install and no account to set up.
🪙 Shulchani Mode
Replace numeric points with Talmudic coin denominations (Darkon, Sela, Dinar, Maah, Pundion, Isar, Prutah). Balances are stored as integer Prutot and displayed as the largest-coin breakdown.
Scan Tab — Tap Panel
Off by default. When on, tapping a name in the list or seating chart on the Scan tab pops up that student's points breakdown (same as the "▾ Breakdown" panel on the Students tab) instead of scoring them — handy if you want to use the Tap Panel to check on a student mid-class without accidentally giving or taking points. Scanning a QR code still always scores normally, even while this is on.
On by default. List / Seating chart is one setting shared by all your classes, but each class has its own chart. So if you arrange one class and leave the panel on Seating chart, switching to a class you haven't arranged yet would otherwise show a grid of empty seats with none of your students in it. With this on, that class shows your class list — alphabetically by last name — with a button to set its chart up when you're ready. The class you did arrange still opens straight to its chart, and nothing about any chart is changed or deleted either way.
Header Shortcuts
On by default. These three float on top of the screen wherever you scroll, and can be dragged anywhere — drop one wherever it's out of your way. Turn this off to hide all three if you don't use the floating points scanner, timer, or picker wheel — the class dropdown and sound button stay either way, and you can always turn this back on here.
How the Picker Opens
Wheel by default. The Picker chooses a boy at random two ways, and this is the one it starts on every time you open it. Wheel spins a colored wheel of names and lands on one. Seating chart shows the class in their seats and flashes from desk to desk before landing on one — the same "Flash & land" every boy can watch from where he's sitting. Both skip boys who already had a turn, and both use the same Instant setting, so this only changes which one is in front of you when the panel opens. You can still switch between them inside the Picker whenever you want — that switch lasts for the rest of the session and doesn't change what you set here.
Matching Games
Drag by default. This sets how both matching games in Review start out — the Pesukim/Mishnayos matching game and the Shorashim match game. Drag means picking a piece up and dropping it on the line it belongs to. Tap means tapping the piece and then tapping its line — usually easier on a touchscreen or a Smart Board, and much easier for a boy who finds dragging fiddly. Both play identically otherwise and the timer and best scores are the same either way. As with the Picker, the Drag / Tap buttons on the games themselves still switch it for the rest of the session without changing what you set here.
Hebrew Date
On by default. Computed by the browser itself — no internet connection needed. If a browser doesn't support it, the Hebrew date is simply left off and nothing else is affected.
Page Width
Off by default. The app normally keeps its content in a centered column, which reads comfortably on a laptop but leaves a lot of empty space on either side of a large desktop monitor. Turn this on to stretch every tab to fill the screen instead — most noticeable on the Scan tab's Customize layout, where it gives the movable sections more room to work with.
6-7 Celebration
Off by default. When on, a scan that brings a student's point total to exactly 67 pops up a short, silent full-screen moment — two hands weighing, a little confetti — then fades away on its own (or tap anywhere to dismiss it early). It fires every time a total lands on exactly 67, so a student who spends points and earns their way back can hit it again. No sound, ever.