尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A tracker for Snap-on® tools at boxtally.app. Not affiliated with Snap-on.
Track the Snap-on tools in your box, what they're worth today, and what's next on your wishlist.
Live site: https://boxtally.app/
Not affiliated with Snap-on.
| Part | Where it lives | What it does |
|---|---|---|
| Website | index.html (the page), app.css (its look), app.js (what it does) and drawings.js (the tool drawings), hosted free on GitHub Pages | The tracker people use. Sign in with Google to sync, or use it without an account (saved in that browser). |
| Database and photos | Supabase (free plan) | Everyone's toolboxes, one shared price list per part number, price history, people's own tool photos, share links and email settings. Setup is in supabase/schema.sql. |
A tool's price today is the typical eBay asking price for a new one (see "eBay prices" below), filled in automatically. Nobody can type in a public price. The only price people enter is what they paid, which is saved on their own item and only they can see (a share link shows it only if they tick that option).
Shared store prices (typed in by anyone tracking a part number) were retired on 2026-10-18 because there was no way to know they were right. The notes below describe the old submit_price function, which the site no longer calls; its execute permission should be revoked (see supabase/schema.sql, end of file).
Safeguards in the submit_price database function:
price_reports (who, what, when, and the price before it) in case something needs undoingEach account can hold up to 2,000 tools (box and wishlist together) and 300 uploaded photos of up to 3 MB each. The site shrinks photos well under that before uploading. The limits keep one account from filling the free Supabase plan.
Import (next to Download CSV, or "Import it" on the welcome) brings in a tool list from a CSV file or rows pasted from Excel, Google Sheets or Numbers. Each column is matched to a tool detail by its heading (part number, name, how many, price paid, date bought, condition, drawer, notes, box or wishlist), and you can change any match before adding. Tools already in your box are skipped and repeats are added up. Up to 500 tools at a time.
Download CSV saves a spreadsheet of the box and wishlist (opens in Excel, Google Sheets or Numbers). Print / PDF prints a clean tool list with totals, or saves it as a PDF from the print dialog. Handy to keep for insurance in case of theft or a shop fire.
The site never loads, copies or hotlinks Snap-on's product photos (Snap-on limits its images to press use).
F80 → 3/8" ratchet, OEXM710B → wrench, TECH3FR250 → torque wrench). Within a kind of tool the drawing shows which one it is: standard, long, stubby or flex-head ratchets; 6-point, 12-point, deep, impact, swivel or bit sockets; combination, ratcheting, flare-nut, box-end, open-end or adjustable wrenches; slip-joint, needle-nose, diagonal, locking, water-pump or snap-ring pliers; hook, straight, 45° or complex picks; flat, Phillips, Torx, stubby or ratcheting screwdrivers. Power tools are split into cordless impacts (3/8", stubby, 1/2" high-torque), impact drivers, drill/drivers, cordless and air ratchets, air impacts, air hammers, die and angle grinders, batteries and chargers. Ratchets also come in hard-handle, round-head, thumbwheel and long flex-head; and there are drawings for extensions, breaker bars, U-joints, adapters, speed handles, hex keys, nut drivers, crowfoot wrenches, test lights, multimeters, pickup tools, service carts and top chests. Each card's label names the kind. The drawings live in drawings.js; the matching rules are ART_WORDS, ART_PN, VARIANT_WORDS and VARIANT_PN.supabase/schema.sql (photo_shared, shared_photos, report_photo) are no longer used by the site./money) with box worth, the truck account, tool insurance, prices and stats. Snap-on's store search shows on the Wishlist. On bigger screens everything shows at once, as before.boxtally.app/?box=…) with an optional title, and choose whether it shows prices paid and the wishlist. It never shows photos, notes, locations, who has a tool, or the owner's name or email. Turning the link off kills it; turning it back on makes a new one. Stored in box_shares, read through the function.boxtally.app/?sample opens the sample box straight away. boxtally.app/?add=F80 puts F80 in the add box (add &list=wishlist for the wishlist). Add ?ref= and a word to any link you post, like boxtally.app/?sample&ref=reddit, to see which place brings people in.?ref= word or the site that linked here (reddit, facebook, google...). Only counts are kept (visit_refs, one number per day per source, through count_visit and the owner-only admin_growth), never who.boxtally.app/t/F80/ and about 4,000 more: the drawing, what the tool is, its Snap-on list and eBay price (loaded live), and Add to my box / wishlist buttons. boxtally.app/t/ lists them all by kind, and sitemap.xml lists every page for Google. They're plain pages made by node checker/tool-pages.mjs from checker/catalog-part-numbers.tsv and the site's own drawing code; run it again after changing drawings or the catalog list. They only show shared catalog facts, never anyone's box.og-image.png is the picture shown when a BoxTally link is pasted into Facebook, iMessage, Reddit or Discord. Bump the ?v= on it in index.html and checker/tool-pages.mjs when it changes, so those sites fetch the new one.GitHub Pages sends the same page for every address, so a pasted share link previews as plain "BoxTally". worker/share-preview.js fixes that: when a link-preview bot asks for boxtally.app/?box=..., it fills in that box's title and tool count ("Mike's box · 63 tools"). People visiting are never slowed down, and it only uses what the share link already shows. To turn it on (free plan):
share-preview, click Deploy.worker/share-preview.js, click Deploy.boxtally.app, route boxtally.app/*, failure mode Fail open. Save.boxtally.app records show an orange cloud (Proxied). Workers only run on proxied records.To turn it off, delete the route.
checker/alerts.mjs runs once a day from .github/workflows/price-alerts.yml. For everyone who ticked Email me when something on my wishlist drops (stored in alert_prefs; the address always comes from the person's own Google sign-in), it emails any eBay price drops on their wishlist since the last email. Drops come from ebay_price_history, which only gets a new point when a price moves 3% or more, so day-to-day wobble doesn't send email.
To switch it on:
SMTP_HOST = smtp.gmail.com, SMTP_PORT = 465, SMTP_USER = that Gmail address, SMTP_PASS = the app password. Optional: ALERT_FROM, e.g. BoxTally <[email protected]>. The workflow also needs SUPABASE_SECRET_KEY (the same secret the price checker uses).EMAIL_ALERTS_READY = true in app.js so the checkbox appears under Price watch.BoxTally names Snap-on only to say which tools it tracks. It doesn't use Snap-on's logo, wordmark or fonts, and the tool drawings are BoxTally's own. The footer and privacy page say it's independent, with the Snap-on® trademark notice.
Free way (works now), managed by the owner: list prices are shared. Accounts in public.admins (the BoxTally owner) see a banner when tools haven't been checked yet, and Prices → Manage list prices opens a box with two tabs: Not checked (part numbers nobody has looked up, newest first) and Checked (with the price, or "no price found"). Copy for Gemini copies 12 part numbers with the question to ask; paste that into the Gemini app (free, it can search Google), then paste its answer back. Each line with a part number and a $ price is saved to list_prices for everyone; a part number with ? is saved as checked with no price, and the Checked tab can copy those again to retry. The owner sees part numbers and how many people track them, never whose tools they are (admin_parts(), admin_save_list_prices()). New part numbers that look like another color or kit of a priced tool (same number once the letters after the last digit are dropped, like CTR868DB and CTR868GMDB) are offered that price in the Not checked tab, with a Use these prices button. Once a day .github/workflows/unpriced.yml (checker/unpriced.mjs) keeps one GitHub issue, "Tools need a Snap-on list price", listing unchecked part numbers; it @mentions the owner when new ones appear (so GitHub emails them) and closes itself when all are checked. When the owner checks a tool and Snap-on's site doesn't list it (a saved "no price"), it uses its eBay price and says "Not listed on Snap-on's site"; Gemini doesn't look those up again. Every color of a tool shares one list price: Snap-on puts the color at the end of the part number (R, O, G, GM, HV, often before DB for tool only, and AM before the color on magnetic pickups), so CDR9050, CDR9050DB, CDR9050HVDB and CDR9050GDB all use whichever one has a saved price, the plain number first (checker/color-family.mjs, copied into app.js and price-now; node checker/color-family.test.mjs). Those aren't sent to Gemini. Anyone can still type a private list price on a tool's edit form (items.list_price, owner-only RLS, not in share links).
A tool's value is: your own list price, else the shared list price, else eBay, else what you paid.
Instant lookups (when a tool is added): the page calls the Supabase Edge Function supabase/functions/price-now for any of a signed-in person's tools that have no eBay or list price yet, right after they add it (and on load). It looks up eBay and the Snap-on list price on the spot, so the card goes from "finding prices…" to a price in seconds. It only does a half when its keys are set in Supabase under Edge Functions → Secrets: EBAY_CLIENT_ID and EBAY_CLIENT_SECRET for eBay, GEMINI_API_KEY for list prices (the same values as the GitHub secrets). Limits keep the bill small: 10 part numbers a call, only tools the caller tracks, 60 eBay and 30 list-price lookups per person per hour, and 400 list-price lookups in any 30 days overall, shared with the GitHub job (price_lookups table). Each lookup runs a few Google searches (Gemini decides how many, sometimes 20+), and 400 keeps them near Google's 5,000 free searches a month. ebay-match.mjs there is a copy of checker/ebay-match.mjs; the eBay test fails if they differ. Deploy with the Supabase CLI or the dashboard after changing it. The Checks workflow pings it on every push.
Automatic way (optional, costs about a cent per new tool):
Snap-on has no public price API, and its store (shop.snapon.com) builds each page in the browser and turns away automated visitors, so BoxTally can't read list prices from it directly (and scraping it would go against Snap-on's terms). Google's search index does have those pages, which is how Gemini can answer "what does F80 cost".
checker/list-prices.mjs runs every 15 minutes from .github/workflows/list-prices.yml (most runs find nothing due and stop right away), so a newly added tool gets its list price within about 15 minutes. For each tracked part number it asks Gemini, with Google Search turned on, for Snap-on's US list price. Gemini 3 models only search through Google's Interactions API (/v1beta/interactions); the older generateContent endpoint quietly skips the search tool and the model answers from memory. If the answer cites a snapon.com page it's saved as the list price. Usually it can't (Google's search results don't say which pages they came from), so a matching answer is saved as Gemini's guess (ai_price): the site shows it marked est., and the owner's Not checked tab lists it with Confirm (or paste a fix). In a 5-tool test against prices the owner had checked, Gemini got 2 exact, 1 within 2%, missed 1 and was 59% off on 1, which is why guesses wait for the owner. --explain prints what Gemini did. Only part numbers someone tracks are looked up, each once for everyone, then rechecked twice a year (Snap-on changes list prices about that often); not-found ones are retried monthly. Each run does at most 40 (MAX_LOOKUPS), new tools first. Prices the owner saved by hand are never overwritten, and a part number the owner marked "no price found" keeps that mark if the lookup misses too. An automatic miss on a new tool stays in the owner's Not checked tab and the daily issue, so a person still picks it up. Google bills Gemini 3 search per query: 5,000 free a month, then $14 per 1,000. The shared 400-lookups-a-month cap keeps it around the free amount; Gemini's own answer costs well under a cent.
On the site, a tool's value is its Snap-on list price when there is one, otherwise the eBay price, otherwise what you paid. The card shows both a "Snap-on list" line (linking to the product on shop.snapon.com) and the eBay line.
Setup: at aistudio.google.com create an API key, and turn on billing for its Google Cloud project (search grounding isn't in Gemini's free tier; the paid tier includes 5,000 searches a month). Add it in the GitHub repo under Settings → Secrets and variables → Actions as GEMINI_API_KEY, then run Snap-on list prices from the Actions tab. A list price an AI found can still be wrong, so the card always links to Snap-on's page to check.
checker/ebay.mjs runs every 15 minutes from .github/workflows/ebay-prices.yml. Each run looks up tools that have never been priced first (so a newly added tool usually has a price within half an hour), then any price older than 3 hours, oldest first, at most 120 per run. That stays well under eBay's 5,000 calls a day.
It uses eBay's official Browse API and saves the typical new and used asking price, the lowest price and the listing count to ebay_prices. Matching lives in checker/ebay-match.mjs (test: node checker/ebay-match.test.mjs). For each part number it takes the first of these that finds something, and saves which one in source:
exact: Buy It Now listings whose title says Snap-on and the exact part number (a part number of 6+ characters counts even without "Snap-on").variant: the same tool under another color or kit ending, or with a 0/O mix-up (CTR881 → CTR881DB, 0FCA1KT → OFCA1KT). note says which.auction: current bids on exact or variant auction listings.similar: an estimate from 3+ listings of sibling part numbers that differ only in the last digit (CTR868 from CTR861/CTR867/CTR869), or for 4+ digit numbers the last two (CT9055 from CT9015/CT9038/CT9080). note says which.
If nothing turns up and the tool had a price before, that price is kept with listings = 0, and the card says "last seen" with the date in seen_at.Whenever a tool's price moves 3% or more from the last day on record, that day's price is saved in ebay_price_history. The site uses it for price drops, sparklines, recent activity and box value over time.
Each tool card shows this as an "eBay:" line with a link to the search. These are asking prices from active listings, not Snap-on's list price or what things actually sold for.
To see why a tool did or didn't match, open Actions → eBay prices → Run workflow, put the part numbers in "parts" and --dry-run --explain in "flags". The log lists every title it saw, marked = exact, ~ variant, - skipped.
Setup: make a free account at developer.ebay.com, create a Production keyset, then in the GitHub repo go to Settings → Secrets and variables → Actions and add EBAY_CLIENT_ID (the App ID) and EBAY_CLIENT_SECRET (the Cert ID). The workflow also needs SUPABASE_SECRET_KEY. Run eBay prices from the Actions tab to fill it right away.
An earlier Snap-on store checker was removed (Snap-on's store hides product data from automated browsers). If you ever set it up on a Windows computer, double-click checker\uninstall-windows.bat to remove its scheduled tasks.
cleanup_old_data, Sundays 09:17 UTC) thins eBay price history older than a year to one price per week..github/workflows/backup.yml copies the whole database every Sunday into the private backups storage bucket (Supabase dashboard > Storage > backups) and keeps the newest 8. To make one now: Actions > Database backup > Run workflow..github/workflows/checks.yml runs on every push: the part number and eBay matching tests, then checker/smoke.test.mjs opens the site at phone and desktop size and loads the sample box. A red X on the commit (and an email) means something broke; the site still publishes, so fix it quickly. To run the smoke test yourself: python3 -m http.server 8080, then node checker/smoke.test.mjs (needs npm install playwright)..github/workflows/uptime.yml loads boxtally.app every 12 hours; if it's down, GitHub emails you..github/workflows/keep-supabase-awake.yml reads one row twice a week so that never happens, even when nobody opens the site. If it does get paused anyway, restore it from the Supabase dashboard; nothing is lost.robots.txt and sitemap.xml help search engines find the site. When the site moves to its own domain, change the addresses in both files (and og:url / og:image in index.html).shared_boxmanifest.webmanifest, icons/ and sw.js let phones and computers install BoxTally. The page, fonts and icons work offline, and a signed-in box is kept in the browser so it still shows (read-only) with no connection. When you change the site, bump VERSION in sw.js and the matching ?v= number on app.css, drawings.js and app.js in index.html (checker/versions.test.mjs fails if they don't match).