Sample School Sample Class 1
Preview mode — scores won't be saved here. You're viewing this inside a browser that blocks local saving. To use it for real, download this file and open it in Chrome on your own computer; everything will then save automatically.

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.

Scan to award points

Scan or tap a student's seat to give points — the seating chart fills the screen so it's easy to read from across the room.

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.

🏆 No contest running right now

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.

Each student's current points are their tickets. Points are not spent — this is just a draw, so the wheel always reflects the live standings.
Tickets from:

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


Tickets each student has:
When dividing evenly, leftover tickets:
Prize:

Learn

Pick a sub-tab to teach, test, or review your text.

Text:
Parsha & range
Now marking
Pick a number above, then scan each student who knew it.

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 kids once. Each gets a QR code you'll print on the QR Labels tab.

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)
  1. Set up the Google Sheet link once on the Sync to Google Sheets tab (if you haven't already).
  2. Make sure you've pasted the latest Code 1 there and re-deployed — it now includes the roster reader.
  3. In Apps Script, pick setupRoster in the function dropdown and click Run once. That creates a Roster tab with the Name / Class headers and a few example rows.
  4. Open your sheet's Roster tab and fill in your kids — one per row, with their class in the second column. List as many classes as you like.
  5. 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, Classor 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.

Print: Seating chart

Prints the current class's seating chart full-page, laid out exactly like the chart on the Scan tab. Empty seats print blank. Uses whichever class is selected in the header.

Preview — this is what will print:

Print: Students

Each student's own QR code, and their own full page of one-scan-does-it-all codes for every activity.

Generating the QR codes needs an internet connection this one time. Daily scanning works offline afterward.

Student labels

Load Avery 5160 (or 8160 / 5136 / 8250, or 18660 clear) address-label sheets. In the print dialog choose Letter paper, set Margins: None and Scale: 100% (turn off "Fit to page"), and print. Do a plain-paper test first and hold it over a label sheet to check alignment.

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.

Format
Groups to print:

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.

Back up everything

Save a file with your full roster, activities, and scores. Keep it somewhere safe.

Download for offline use

Saves a single HTML file with your current students, activities, and scores already loaded in — plus the QR scanner built right in, so it works with zero internet from then on. Needs internet once, to prepare the file; after that, just open it in any browser like any other file.

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.

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 bunk, a new summer). 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)
  1. Open sheets.google.com and make a new blank sheet. Name it (e.g. "Sample Class 1 Points").
  2. In that sheet's menu: Extensions → Apps Script.
  3. 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.
  4. Delete any code in the editor and paste in Code 1 below.
  5. Click the gear (Project Settings) on the left and tick "Show appsscript.json manifest file in editor."
  6. 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.
  7. Click Deploy → New deployment. Type: Web app. Set Execute as: Me and Who has access: Anyone. Click Deploy.
  8. 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.)
  9. 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 1 — paste into Code.gs

Already set up? Two steps, not one:

  1. Re-copy this updated Code 1 and paste it over your existing Code.gs (replacing it entirely), then save (Ctrl/Cmd+S).
  2. 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.)
This is what actually turns on Attendance, Tracker, and Homework syncing — they write to new "Attendance Log", "Tracker Log", and "Homework Log" tabs, which only get created after the redeploy step above.

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);
  }
  return ContentService.createTextOutput("Points sync is live.");
}

// Log columns are: Time, Student, Bunk/Group, Activity, Points, New total, ID, Undo Of.
// 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", "Bunk/Group", "Points"]);
  list.forEach(function (it, i) { lb.appendRow([i + 1, it.name, it.group, it.total]); });
  lb.setFrozenRows(1);
}

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);

  if (body.type === "scan" || body.type === "correction") {
    var log = ss.getSheetByName("Log") || ss.insertSheet("Log");
    if (log.getLastRow() === 0) {
      log.appendRow(["Time", "Student", "Bunk/Group", "Activity", "Points", "New total", "ID", "Undo Of"]);
    }
    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 || ""]);
      rebuildLeaderBoard();
    }
  }

  if (body.type === "standings") {
    var st = ss.getSheetByName("Standings") || ss.insertSheet("Standings");
    st.clear();
    st.appendRow(["Rank", "Student", "Bunk/Group", "Points"]);
    (body.rows || []).forEach(function (r) { st.appendRow(r); });
  }

  if (body.type === "pasuk") {
    var pl = ss.getSheetByName("Pesukim Log") || ss.insertSheet("Pesukim Log");
    if (pl.getLastRow() === 0) {
      pl.appendRow(["Time", "Student", "Bunk/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", "Bunk/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);
  }

  if (body.type === "attendance") {
    var att = ss.getSheetByName("Attendance Log") || ss.insertSheet("Attendance Log");
    if (att.getLastRow() === 0) {
      att.appendRow(["Time", "Date", "Student", "Bunk/Group", "Status"]);
    }
    att.appendRow([new Date(body.ts), body.date, body.name, body.group, body.status]);
  }

  if (body.type === "tracker") {
    var trk = ss.getSheetByName("Tracker Log") || ss.insertSheet("Tracker Log");
    if (trk.getLastRow() === 0) {
      trk.appendRow(["Time", "Student", "Bunk/Group", "Activity"]);
    }
    trk.appendRow([new Date(body.ts), body.name, body.group, body.activity]);
  }

  if (body.type === "hw") {
    var hwl = ss.getSheetByName("Homework Log") || ss.insertSheet("Homework Log");
    if (hwl.getLastRow() === 0) {
      hwl.appendRow(["Time", "Date", "Student", "Bunk/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", "Bunk/Group", "Activity", "Points", "New total", "ID", "Undo Of"]);
  }
  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 kids 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"]);
  }
}
Code 2 — paste into 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

בָּרוּךְ אַתָּה יְיָ אֱלֹהֵינוּ מֶלֶךְ הָעוֹלָם, בּוֹרֵא נְפָשׁוֹת רַבּוֹת וְחֶסְרוֹנָן, עַל כָּל מַה שֶּׁבָּרָאתָ לְהַחֲיוֹת בָּהֶם נֶפֶשׁ כָּל חָי. בָּרוּךְ חֵי הָעוֹלָמִים.

Contest

Run a point contest for a set window of time, with its own history — separate from the regular Standings/Trends view.

🏆 No contest running right now.

Past Contests

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

Student:

Recent purchases

Bathroom Passes

Each student gets 1 pass per week. Scan a student's QR code when they use it. Reset the passes at the start of each week.

Scan or type a student. Each one can only use their pass once per week.

Attendance

Everyone defaults to Present — tap a student to mark them Absent or Late instead. Each change is time-stamped and synced to your Google Sheet if connected.

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.

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.

Baud rate:
Not connected.

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

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.

Header Shortcuts

On by default. Turn this off to keep the header simpler 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.

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.

Teach Mode

Off by default. Normally ← advances to the next phrase (since the text reads right-to-left). Turn this on to reverse it so → advances instead — easier if you're not used to RTL navigation.

Off by default. In Teach mode each phrase row shows Hebrew then English side-by-side. Turn this on to put the Hebrew on the right and the English on the left — a more natural layout for RTL readers.

Shorashim — Quiz

After a boy answers, the quiz waits this long (so he sees right/wrong) before moving on. Lower it to speed up; 0 jumps instantly.

Shorashim — Game Show

Gives you time to scan the boys in the correct corner before the next question appears. Starting a question manually cancels any pending auto-advance.