Line numbers refer to ironfist911.html as distributed (11,943 lines). Function names are as they appear in the source. A leading underscore marks a helper not meant to be called from markup. The section numbers (1.1, 5.11, and so on) are the file's own; its module map at line 1182 lists them, and searching the source for a number jumps to that section.
B.1 Architecture
File layout. One HTML document, no external requests. Lines 1 to 23 are document metadata (Dublin Core, citation, canonical DOI). Lines 24 to 26 are a JSON-LD block typing the file as SoftwareSourceCode, WebApplication, and ScholarlyArticle. Lines 27 to 634 are CSS, light and dark themes as CSS custom properties. Lines 636 to 1132 are markup: the three panes (field form, saved-record list, office tools) and the compiler drawer. Lines 1133 to 1173 hold the QR generator as its own plain script. Lines 1174 to 10706 are the main program, Parts 1 to 8. Lines 10707 to 10936 are the dialogs. Lines 10937 to 11689 are the compiler drawer (Part 9), a second script in its own closure. Lines 11691 to 11938 are the pane shell (Part 10), a third.
Runtime layers. Four things carry the state; everything else reads through them.
FIELD_DEFS (line 1312) is the schema. Each entry maps an internal key to a CSV/Darwin Core column name and a human label, with an optional type of boolean or number. Every exporter, importer, form reader, and the HTML table iterate this one array. _coerceFieldValue() (line 1461) is the only place typed values are converted on the way in from flat formats.
Store (line 1599) is the persistence kernel: a single localStorage key (fieldVoucherRecords_v1, line 1573) holding an object of typed lists (voucher, addendum, uiState, list, bundle). Operations are all, get, put (upsert by id), remove, replaceAll, mutate, filter, lastWriteOk, and invalidateCache. Every write re-reads disk first, so two tabs on one workspace do not overwrite each other; mutate (line 1715) is the read-modify-write form. A refused write sets a flag and routes to the storage failure reporter. Upsert-by-id is the property the merge model rests on: two devices generating different UUIDs can union their exports without collision, and re-importing the same file is idempotent.
Bus (line 1797) is a minimal publish/subscribe with on and emit. The list, gallery, problems note, and workspace bar redraw on vouchers:changed; addenda:changed and draft:restore work the same way. Listeners are wrapped so one failing listener cannot stop the rest.
Draft (line 5074) is a plain object mirroring the form. Delegated input and change listeners update it as the user types; bulk writers (reset, restore, duplicate, GPS, EXIF) call _draftSyncFromDOM() (line 5076) immediately after setting values, because programmatic .value assignment fires no input event. voucherFromForm() (line 5384), the save gate, and the autosave all read Draft rather than the DOM.
Workspaces. One URL parameter, ?ws=name (line 1296), turns the file into separate data sets. Every storage key gets the workspace name appended through _wsKey(); the default workspace keeps the bare keys, so data from earlier builds is where it always was. The list of known workspaces and the install's device tag are shared on purpose.
Identifiers. Three UUIDs with three lifespans, all minted by uid() (line 1873, RFC 4122 v4 via crypto.randomUUID, falling back to getRandomValues, then to Math.random). occurrenceID is one per specimen, immutable, minted at draft start by voucherOccurrenceUUID() (line 2777), and is the foreign key for addenda and the dedupe key for merges. The Voucher ID (id) is the label and QR number; it is generated per draft but may be replaced by a preprinted sheet, and photos are re-keyed when it changes. sessionId() (line 2016) is one per browser install, stored under the legacy name deviceTag for backward compatibility, and stamped on every voucher and addendum at save. compileBatchId is one per compiler clear, stamped on compiled exports only.
Storage keys. Records: fieldVoucherRecords_v1 (line 1573). Draft autosave: fieldVoucherDraftAutosave_v1 (line 5115). Pre-migration backup of URL-shaped IDs: fieldVoucherreport_redirect_backup_v1 (line 2896). Photos: IndexedDB database fieldVoucherPhotos_v1, store photos (lines 3299 to 3300), holding Blobs keyed by photo id with a voucherId index. All four take the workspace suffix. Shared across workspaces: fieldVoucherWorkspaces_v1 (line 1303) and fieldVoucherDeviceTag_v1 (line 2006). Legacy keys read once by migration and then ignored: fieldVoucherreport_v1 (line 1304), fieldVoucherCollectors_v1 (line 4159), fieldVoucherDwcExtraFields_v1 (line 4318). Record schema version constant: RECORD_SCHEMA_VERSION = "v2" (line 1579).
Addenda model. Corrections are their own Store type (addendum), each with its own UUID, a parentOccurrenceID, a parentVoucherId, a type of georeference or redetermination, a payload, an author, a device tag, and a timestamp. The voucher row is never rewritten. computeCurrentStateFields() (line 2159) derives the current_* columns: the standing redetermination, and the standing georeference by supersession rather than array order (_geoHeads(), line 2259). Decoration happens at export and render time, never at write time. DERIVED_STATE_KEYS (line 2219) is computed from the same function so the two cannot drift.
One door in. canonicalRecord() (line 2522) is the single entry for every saved and imported record: FIELD_DEFS keys plus four named extras, so an unrecognized column cannot ride into the store. addRecords() (line 2631) is the single import path for JSON, CSV, and take-home bundles. The compiler drawer uses the same canonicalRecord().
Export and import symmetry. JSON nests the addenda array. CSV, XLSX, and HTML serialize the same array into the addenda_json column. parseAddendaField() (line 2137) accepts either shape, so a flat round trip is chain-safe.
Ways out. Files: CSV, JSON, XLSX, photo ZIP, HTML table, email, labels, QR sheets, and QR tags (Part 5). Take it home (section 5.5b, line 7131) writes one ZIP per part holding a viewer page, the JSON and CSV exports, the photos, a manifest with a SHA-256 for every file, and a README; a GitHub route cuts parts under GitHub's browser limits (line 7171). Import bundle (section 5.5c, line 7713) brings a bundle back and checks every photo against its manifest hash before reattaching it. Collection exports for Symbiota, Specify, and DiSSCo share one reader, _csPrepare() (line 8820).
Init sequence (lines 10625 to 10705). Tracer install, kernel migration from legacy keys, theme, dates, kingdom, pristine snapshot, form listeners, Voucher ID and occurrenceID, Darwin Core fields, collectors, autofill guards, section state, autosave check, trip strip, timer arming listeners (click, input, change, capture phase), legacy URL-ID migration followed by vouchers:changed, store reconciliation and the snapshot offer, storage resilience check, the self test when asked for, session badge, top stamp, workspace bar, two-tab presence, and the associated-voucher readout.
Third-party code. One item: Kazuhiko Arase's QR Code Generator for JavaScript, MIT, vendored at lines 1133 to 1173. It runs as its own script and sets window.QR; _ensureQR() (line 2747) only confirms it is there. Everything else, ZIP reader and writer, XLSX writer, EXIF reader and writer, JPEG re-encoder, SHA-256 through the browser's crypto.subtle, is in-file.
B.2 Function reference
All 488 functions in the file, in source order, grouped by the file's own sections. Each description is the one-line comment above the function in the source.
1.1 Workspaces (lines 1278 to 1306)
_wsSanitize(v), line 1295. Folds any workspace name to lowercase letters, digits and hyphens, 24 characters max.
_wsKey(k), line 1300. Storage key for this workspace; the default workspace keeps the bare key.
_wsFileName(n), line 1302. Download filename for this workspace: pests_field_vouchers_2026-09-19.csv.
1.2 Field definitions (lines 1307 to 1526)
_coerceFieldValue(def, value), line 1461. Turns a raw cell value into the type its FIELD_DEFS entry declares (boolean, number, text).
_boolTrue(v), line 1481. True for true, "true", 1 or "1"; everything else is false.
_mergeAddendumCopies(kept, incoming), line 1493. Two copies of the SAME addendum (same id) that disagree on isCurrentIdentification.
1.3 Kernel: Store and Bus (lines 1527 to 1756)
_load(fresh), line 1621. Reads the whole record store from localStorage, using the cache when nothing has changed.
_save(), line 1648. Writes the cache to localStorage and reports a refused write instead of swallowing it.
all(type), line 1664. Every record of one type (a copy, safe to change).
get(type, id), line 1666. One record by type and id, or null.
put(type, id, data), line 1671. Adds or replaces one record, then saves.
remove(type, id), line 1683. Removes one record by type and id, then saves.
replaceAll(type, records), line 1704. Swaps out every record of one type in a single write. 801: this re-reads disk and then throws the re-read away.
mutate(type, fn), line 1715. Read, modify, write against what is on disk NOW. fn receives the current list (a copy, safe to mutate) and returns the list to store; returning nothing keeps the copy it was handed.
filter(pred, type), line 1726. Every record that passes a test, across one type or all of them.
lastWriteOk(), line 1737. Did the last write to localStorage succeed?
invalidateCache(), line 1743. Drops the parsed copy so the next read goes back to disk.
1.4 Storage failure reporting (lines 1757 to 1811)
storageWriteFailed(), line 1768. True while localStorage is refusing writes.
reportStorageWriteFailure(err, what), line 1770. Records a failed write and warns the user once.
noteStorageWriteRecovered(), line 1789. Clears the storage warning once writes succeed again.
on(evt, fn), line 1800. Subscribes a function to a named event.
emit(evt), line 1802. Calls every function subscribed to an event; one failing listener never stops the rest.
1.5 Util (lines 1812 to 1889)
esc(s), line 1816. Escapes text for safe use inside HTML.
_forceLowercase(el), line 1822. Species epithets are never capitalized in botanical nomenclature, but autocapitalize="off" alone is not honoured by every mobile keyboard, so this forces the value itself lowercase as it's typed rather than trusting the attribute.
todayISO(), line 1841. Today's date from local calendar parts, not UTC, so an evening record is not dated tomorrow.
fullName(first, last), line 1849. First and last name joined into one display name.
_isoDateInputFilter(el), line 1857. Keeps a typed date in YYYY-MM-DD form as the user types digits.
uid(), line 1873. A random v4 UUID, with fallbacks for older browsers.
1.6 Legacy to Store migration (lines 1890 to 1948)
_kernelMigrateLegacyIfNeeded(), line 1897. One-time move of old per-feature storage keys into the Store.
2.1 Addenda (lines 1949 to 2320)
_sharedDeviceTag(), line 2008. The install-wide copy of the tag, or "" if none or unreadable.
_rememberSharedDeviceTag(tag), line 2012. Writes the install-wide copy of the tag, only if it is still empty.
sessionId(), line 2016. The per-install session tag: reads it, borrows the shared copy, or mints one on first call.
createAddendum(parentOccurrenceID, parentVoucherId, addendumType, payload, authorFirst, authorLast), line 2038. Files a new addendum (redetermination, georeference, note) against a saved voucher.
getAddendaFor(occurrenceID), line 2081. Every addendum for one occurrenceID, oldest first.
deleteAddendaForOccurrences(occurrenceIDs), line 2094. Deletes the addenda of vouchers being deleted, so none are left pointing at a missing occurrenceID; returns the count.
deleteAllAddenda(), line 2103. Removes every addendum in this workspace; returns how many went.
countAddendaForOccurrences(occurrenceIDs), line 2110. Counts the addenda that point at any of these occurrenceIDs.
deleteAddendum(id), line 2116. Deletes one addendum after a confirm; the voucher itself is untouched.
_normalizeAddendum(a), line 2130. Pulls the addenda chain off an incoming record whatever format carried it in: JSON nests the real array under .addenda, the flat formats carry the same array serialized in the addenda_json column.
parseAddendaField(src), line 2137. Reads the addenda chain from an imported record, whether nested JSON or an addenda_json cell.
computeCurrentStateFields(rec, addenda), line 2159. Works out the current name, determiner and coordinates from a voucher and its addenda.
decorateWithCurrentState(rec), line 2223. A saved voucher plus its live addenda and current-state fields.
decorateAllWithCurrentState(recs), line 2228. decorateWithCurrentState() over a list.
decorateWithNestedAddenda(rec), line 2234. Current-state fields from addenda already on the record (no Store lookup).
decorateAllWithNestedAddenda(recs), line 2239. decorateWithNestedAddenda() over a list.
_geoHeads(addenda), line 2259. The standing georeferences for one specimen, the ones nothing supersedes. One head is current; more than one is a conflict.
_geoLineage(head, addenda), line 2278. The standing georeference followed by everything it was made on top of, newest first, using the same supersession rule as _geoHeads().
findAddendumProblems(vouchers, addenda), line 2295. Finds orphaned addenda and specimens with zero or several current determinations.
findLocalAddendumProblems(), line 2317. findAddendumProblems() over this workspace's own saved data.
2.2 Saved list and import checks (lines 2321 to 2486)
load(), line 2325. Every saved voucher in this workspace.
persist(arr), line 2327. Replaces the saved voucher list.
persistUpdate(fn), line 2332. The read-modify-write form of persist(). Use this for anything that edits the current set (add one, delete one, delete these); persist() stays for a genuine wholesale replacement. fn gets the list as it is on disk at this instant, not as it was when the screen last drew.
_uniqueList(arr), line 2334. Distinct non-empty values, sorted.
_shortIdList(arr, max), line 2336. A short comma list of IDs for messages, with "+N more" when long.
_existingVoucherIdSet(records), line 2341. The set of Voucher IDs already in a record list.
_voucherIdInUse(id, records), line 2347. Is this Voucher ID already taken?
_freshUniqueId(used), line 2349. A new UUID not in the given set; adds it to the set.
_findIncomingIdClashes(items, existingRecords), line 2356. Lists incoming Voucher IDs that clash with saved ones or repeat inside the import.
_blockDuplicateImportIfNeeded(items, existingRecords), line 2369. Stops an import with a message if any Voucher ID would clash.
ingestAddendaFor(item, rec), line 2400. The import half of the addenda fix, and the actual bug: export nested the chain correctly, import ran every record through FIELD_DEFS and threw the nested array on the floor.
_malformedAddendaCell(src), line 2424. True when an addenda_json cell is present but not a readable list.
_addendaIntegrityIssues(src), line 2436. The dialog runs _addendumFormatProblems() on every addendum typed here; an addendum arriving in a file never did, so a georeference with latitude 999 imported clean and became current.
_recordIntegrityIssues(src), line 2448. Format problems in one record (bad coordinates, bad dates) for the import flag.
_joinImportIssues(existing, added), line 2480. Merges two lists of import issues into one "; " string without repeats.
2.3 Canonical record (lines 2487 to 2742)
canonicalRecord(raw, opts), line 2522. Builds one clean record from any input, field by field through FIELD_DEFS.
_fingerprintSource(rec), line 2591. The exact string every fingerprint is taken over.
_recordFingerprint(rec), line 2602. A content fingerprint of one record, used to spot exact duplicates on import.
_recordFingerprintLegacy(rec), line 2615. The pre-801 low-byte hash, kept so old stamps still verify.
_fingerprintMatches(rec, stamp), line 2625. True when a stored fingerprint still matches, old format or new.
addRecords(items), line 2631. Adds imported records to the saved list, merging, de-duplicating and ingesting addenda.
2.4 Voucher identity and QR engine (lines 2743 to 3135)
_ensureQR(cb), line 2747. The QR library runs as its own plain script at page load and sets window.QR; this just confirms it is there, then runs the callback.
voucherId(), line 2756. The draft's current Voucher ID (the QR value).
generateVoucherQR(optId), line 2758. Sets the draft's Voucher ID and draws its QR code.
voucherOccurrenceUUID(), line 2777. The draft's occurrenceID, created on first use.
_draftIdentityKey(), line 2784. Which draft is on screen. The occurrenceID is minted fresh for every new draft and restored with a restored one, and a relabel (new Voucher ID) leaves it alone, so it changes exactly when the specimen changes.
newDraftOccurrenceUUID(), line 2786. Starts a fresh occurrenceID for a new draft and shows it.
queuePhotoIdMigration(oldId, newId), line 2808. Moves the draft's photos to a new Voucher ID, one move at a time.
refreshVoucherQR(), line 2824. Mints a fresh Voucher ID for the draft and redraws its QR.
downloadVoucherQR(), line 2833. Saves the draft's QR code as a PNG.
makeCardQR(id, px), line 2845. A QR code as a PNG data URL, for cards and printouts.
_voucherQrMode(mode), line 2854. Switches the QR panel between a generated ID and a pre-printed sheet ID.
_cleanVoucherId(id), line 2868. Strips control characters and spaces from a Voucher ID.
_voucherIdIsUrl(id), line 2891. True when a Voucher ID is a web address or other resolvable link (not allowed).
async migrateLegacyRedirectVoucherIds(), line 2898. One-time repair: replaces URL-style Voucher IDs from old builds with UUIDs, keeping a backup.
_assignPreprintId(id), line 2966. Takes a scanned or typed pre-printed ID as the draft's Voucher ID.
async startVoucherScan(), line 2997. Opens the camera and starts looking for a QR code.
_doScanFrame(), line 3019. Checks one video frame for a QR code, then schedules the next.
_stopScanStream(), line 3036. Stops the camera and frame loop.
_qrModalSetTarget(t), line 3054. Sets the scan dialog's wording for its job: pre-printed sheet or associated voucher.
openQRModal(mode, target), line 3063. Opens the scan / type ID dialog.
closeQRModal(), line 3074. Closes the scan / type ID dialog and stops the camera.
_qrModalKeydown(e), line 3090. Escape closes the scan dialog.
_qrModalBackdropClick(e), line 3092. A click outside the box closes the scan dialog.
qrModalTab(mode), line 3094. Switches the scan dialog between Scan and Type tabs.
voucherManualIdPreview(), line 3114. Previews a typed ID as a QR code while typing.
voucherAssignManualId(), line 3121. Accepts the typed ID, for the draft or for an associated voucher link.
3.1 Coordinates (lines 3136 to 3260)
_setNoFixFromGPS(reason, displayReason), line 3140. Records a failed GPS attempt without wiping coordinates already entered.
useCurrentLocation(), line 3180. GPS button: the location prompt appears here, on the press, and nothing else prompts.
markManualCoordEntry(), line 3246. Marks coordinates as typed by hand and notes the source.
_coordNoFixClean(), line 3256. True when GPS failed and the coordinate boxes are still empty.
3.2 Photo store (lines 3261 to 3924)
_openPhotoDB(), line 3305. Opens (or creates) the photo database.
_photoId(), line 3335. A unique id for one stored photo.
async resizeImageFile(file, maxDim, quality, stampId), line 3349. Shrinks and re-encodes a photo to JPEG, stamping the Voucher ID into it.
_stampPhotoId(ctx, w, h, id, coverId), line 3400. Burns the Voucher ID into the photo's pixels, sized so a long ID fits on a small photo.
async _restampPhotoBlob(blob, oldId, newId), line 3431. Re-draws a photo's stamped ID after the voucher's ID changes.
sanitizeFilename(s), line 3461. Makes text safe to use as a file name.
_asciiBytes(s), line 3475. Text as null-terminated ASCII bytes, for EXIF fields.
_buildVoucherExifSegment(id), line 3483. Builds an EXIF block that carries the Voucher ID.
entry(tag, type, count, value), line 3507. Writes one EXIF directory entry.
_isExifApp1(bytes, off), line 3533. True when these bytes start an EXIF APP1 segment.
_stripExifApp1(bytes), line 3538. Removes any existing EXIF segments from a JPEG.
async _jpegWithVoucherExif(blob, id), line 3558. Puts the Voucher ID EXIF block into a JPEG.
async _jpegHasVoucherExif(blob, id), line 3569. Does this JPEG already carry this Voucher ID in its EXIF?
_photoExt(p), line 3600. The file extension that matches what is actually stored.
photoFilename(idVal, index, p), line 3611. <voucherId>_NN.<ext> for one photo in an export.
stablePhotoNames(voucherIdVal, photos, rec), line 3625. Photo id to filename for a voucher's photos. A photo keeps the first name it went out under; a new photo takes the lowest unused number.
async addPhotosToVoucher(voucherIdVal, files), line 3647. Saves photos to the database under one Voucher ID.
async getPhotosForVoucher(voucherIdVal), line 3687. Every stored photo for one Voucher ID.
async photoVoucherIdCounts(), line 3745. Every voucherId that has at least one photo row against it, with a count.
async deletePhoto(photoId), line 3766. Deletes one stored photo.
async deletePhotosForVoucher(voucherIdVal), line 3775. Deletes every photo for one Voucher ID.
async clearAllPhotos(), line 3787. Deletes every stored photo in this workspace.
async migratePhotosVoucherId(oldId, newId), line 3799. Moves photos from an old Voucher ID to a new one, re-stamping each.
async renderPhotoGallery(), line 3829. Draws the thumbnail strip under the Photos field.
async deletePhotoUI(photoId), line 3853. Removes one photo after a confirm.
onPhotoSkipToggle(), line 3859. Shows or hides the "no photo" reason box.
openPhotoInput(which), line 3888. Opens the camera or the picker from a real user press.
async handlePhotoInput(inputEl), line 3897. Handles photos picked in the form: store, stamp, check GPS.
3.3 EXIF GPS (lines 3925 to 4078)
async extractExifGPS(file), line 3944. Reads GPS coordinates from a JPEG's EXIF, or null.
_parseExifGPS(view, tiffBase), line 3967. Walks the EXIF directories to the GPS tags and returns decimal lat/lon.
_haversineMeters(lat1, lon1, lat2, lon2), line 4027. Distance in meters between two lat/lon points.
async reconcileExifGPS(files, draftKey), line 4034. Compares photo GPS with the form's coordinates; fills or flags as needed.
3.4 Voucher timer (lines 4079 to 4146)
beginVoucherTimer(), line 4113. Starts the draft's timer.
disarmVoucherTimer(), line 4117. Clears the draft's timer.
_isDraftSurface(el), line 4126. True when an element is part of the draft (form or QR block).
_armVoucherTimerOnFirstAction(ev), line 4131. Starts the timer on the first real edit to the draft.
3.5 Collectors (lines 4147 to 4305)
_collectorsLoad(), line 4165. The saved collector list.
_collectorsPersist(), line 4170. Saves the collector list.
_collectorsSyncFromDOM(), line 4175. Copies typed collector names from the form into memory.
_collectorsSyncAndSave(), line 4183. Reads, saves and passes collector changes on to the trip strip and Identified-by.
_syncDetFromCollector(), line 4196. Mirrors the primary collector's name into the Identified-by (det) fields: the collector is the default determiner at time of discovery.
renderCollectorRows(), line 4209. Draws the collector rows.
addCollectorRow(), line 4241. Adds an empty collector row.
removeCollectorRow(id), line 4250. Removes a collector row, with Undo.
moveCollectorRow(id, dir), line 4268. Moves a collector up or down the list.
getCollectorNamesList(), line 4281. Every collector's full name, primary first.
getPrimaryCollectorFirst(), line 4286. The primary collector's first name.
getPrimaryCollectorLast(), line 4288. The primary collector's last name.
getAdditionalCollectorsFromForm(), line 4290. The other collectors, joined with " | ".
_restoreExtraCollectorValues(), line 4294. Redraws the collector rows from memory.
initCollectors(), line 4298. Loads saved collectors and draws their rows at startup.
3.6 Additional Darwin Core fields (lines 4306 to 4572)
_dwcLoadSchema(), line 4321. The saved list of added Darwin Core terms.
_dwcSaveSchema(), line 4326. Saves the list of added Darwin Core terms.
_dwcSafeId(term), line 4330. A Darwin Core term made safe for use in an element id.
_dwcCategoryFor(term), line 4332. The picklist category a Darwin Core term belongs to.
_dwcAllTermsFlat(), line 4337. Every Darwin Core term in the picklist, with its category.
filterDwcTerms(), line 4344. Filters the Darwin Core picklist as the user types.
hideDwcTermDropdown(), line 4365. Hides the Darwin Core picklist.
selectDwcTerm(term), line 4367. Adds the chosen Darwin Core term to the form.
_dwcAddRow(term, value), line 4379. Draws one added Darwin Core row, with an optional value.
removeDwcExtraField(term), line 4395. Removes an added Darwin Core row, with Undo.
getDwcExtraObjectFromForm(), line 4412. The added Darwin Core values as {term: value}.
packDwc(obj), line 4422. {term: value} packed as "term: value | term: value".
getDwcExtraFieldsFromForm(), line 4426. The added Darwin Core values, packed.
_parsePackedDwc(str), line 4453. The packed extraDwcFields string as a term/value object.
dwcShapeConflicts(rec), line 4462. Terms where a record's dwc object and its packed string disagree.
parseExtraDwc(rec), line 4473. A record's added Darwin Core terms, from either shape or both.
dwcColumnsFor(recs, opts), line 4499. The added Darwin Core columns for an export, in a stable order: this workspace's configured terms first, then any others the records carry.
exportFieldDefs(recs, opts), line 4520. The export column list: FIELD_DEFS with added Darwin Core columns in place.
flattenDwc(rec), line 4528. One record with its added Darwin Core values spread into their own columns.
flattenAllDwc(recs), line 4535. flattenDwc() over a list.
dwcTermForHeader(h), line 4541. Matches an import column header to a Darwin Core term, any case.
dwcFromColumns(headers, cells, taken), line 4549. Collects Darwin Core values from import columns nothing else claimed.
attachDwc(rec, src, fromCols), line 4561. Puts the Darwin Core values on a record, both as an object and packed.
initDwcExtraFields(), line 4568. Restores the added Darwin Core rows at startup.
3.7 Associated voucher (lines 4573 to 4773)
_dwcEnsureTerm(term), line 4600. Adds a Darwin Core row to the form if it isn't there; returns its input.
_assocParts(v), line 4605. Splits a " | " list into parts.
_assocSet(el, parts), line 4607. Writes a DwC row's value the way typing would, so Draft and the autosave see it.
invalidateWorkspaceIndex(), line 4636. Drops the cross-workspace index so the next lookup rebuilds it.
_workspaceIndex(), line 4638. The parsed index of every other workspace's vouchers, built once and reused.
unreadableWorkspaces(), line 4667. The workspaces whose record store could not be read just now.
findVoucherAnywhere(idOrOcc), line 4669. Finds a voucher by Voucher ID or occurrenceID in any workspace here.
_assocRel(), line 4677. The relationship, tidied: no parentheses or bars, which would break the DwC string.
linkAssociatedVoucher(raw), line 4683. The link itself: look the scanned ID up, write the DwC rows, show it.
unlinkAssociatedVoucher(i), line 4709. Removes one link, and its associatedTaxa entry when there is one.
renderAssocReadout(), line 4727. One line per link: relationship, name, and where it lives.
toggleOtherTaxa(force), line 4756. The Other taxa bar under Additional Darwin Core field: opens and closes the associated-voucher block.
initAssociatedVoucher(), line 4765. Restores the associated-voucher controls at startup.
3.8 Suggestions (lines 4774 to 4936)
renderOptionDropdown(box, items, rowHtml), line 4784. Fills a suggestion box with rows, or hides it when empty.
hideDropdown(boxId), line 4791. Hides a suggestion box.
_recentValuesFor(key, max), line 4804. Recent distinct values of one field across saved vouchers.
showRecentSuggestions(fieldId, boxId, key), line 4813. Shows recent values under a field.
hideRecentSuggestions(boxId), line 4826. Hides recent values.
_recentPick(fieldId, idx), line 4828. Puts a picked recent value into its field.
_familyCandidates(), line 4862. Family names to suggest: this workspace's own first, then the seed list.
showFamilySuggestions(), line 4875. Shows family suggestions as the user types.
_familyPick(i), line 4889. Puts the picked family into the field.
_familyKey(e), line 4899. Arrow keys walk the list, Enter or Tab takes the highlighted row (Tab takes the top row when none is highlighted), Escape closes it.
updateRecentCollectorNames(), line 4919. Refreshes the collector name suggestions from saved vouchers.
3.9 Trip strip (lines 4937 to 4955)
renderTripStrip(), line 4943. Draws the trip strip: collectors and institution for this sitting.
3.10 Duplicate for this site (lines 4956 to 5059)
_legacyAdminKeys(o), line 4982. Maps the old State/Province and County/Parish draft keys onto their current names.
_siteFromForm(), line 4992. The site fields (place, coordinates, habitat) from the form.
_siteHasContent(site), line 5004. True when a site has a locality or coordinates.
rememberSite(site), line 5009. Remembers the last site for Duplicate for this site.
lastRememberedSite(), line 5013. The last remembered site, or null.
_applySite(site), line 5018. Writes a site back into the form.
_draftHasSpecimenContent(), line 5030. True when the draft has any specimen-level content.
async duplicateForThisSite(), line 5034. Starts a new draft at the same site as the last one.
3.11 Draft state (lines 5060 to 5086)
_draftSyncFromDOM(), line 5076. Copies every form field into Draft.
draftVal(id), line 5085. One draft value: from Draft, else from the form.
3.12 Draft autosave (lines 5087 to 5294)
_draftIdentitySnapshot(), line 5121. The draft's IDs, photo choice and added fields, for the autosave.
_draftAutosaveNow(), line 5142. Writes the draft autosave now.
scheduleDraftAutosave(), line 5161. Autosaves the draft shortly after typing stops.
clearDraftAutosave(), line 5166. Deletes the draft autosave.
capturePristineDraft(), line 5184. Notes what a blank form looks like, so the autosave can tell real edits.
_draftHasContent(data), line 5189. True when a draft differs from a blank form.
formHasUnsavedContent(), line 5215. True when the form holds unsaved work.
checkForDraftAutosave(), line 5230. Offers to restore a saved draft at startup.
_showDraftRestoreBanner(data, photoCount), line 5249. Shows the restore banner for a saved draft; photoCount > 0 says photos are the reason.
restoreDraftAutosave(), line 5278. OWNERSHIP, stated once so the next person reading this doesn't have to infer it from call sites: Store owns record state.
discardDraftAutosave(), line 5285. Throws away the saved draft and hides the banner.
dismissDraftRestoreBanner(), line 5290. Hides the restore-draft banner.
3.13 Apply a restored draft (lines 5295 to 5381)
applyDraftSnapshot(data), line 5301. Writes a restored draft back into the form.
3.14 Form (lines 5382 to 5441)
voucherFromForm(), line 5384. Builds a voucher record from the form.
3.15 Save gate (lines 5442 to 5844)
isNotRecorded(v), line 5487. True when a value is the "Not recorded" placeholder.
recordedOrBlank(v), line 5489. A value, or blank if it is "Not recorded".
fillNotRecorded(v), line 5491. Fills empty optional fields with "Not recorded"; returns which.
clearFieldHighlights(), line 5500. Removes red highlights from form fields.
_isRealISODate(s), line 5514. The gate tested non-empty and stopped there, so latitude=banana, longitude=x and 2026-99-99 all sailed through and landed in the record, the export, and GBIF's lap.
_coordProblem(raw, limit, name), line 5524. A message if a coordinate is not a valid decimal number in range, else "".
validateFieldFormats(), line 5533. Checks coordinate and date formats before a save.
validateRequiredFields(), line 5559. Lists required fields that are empty.
async validatePhotoRequirement(voucherIdVal), line 5599. The photo rule at save: a photo, or No photo ticked, never both.
showIncompleteVoucherModal(missing), line 5615. Shows the incomplete-voucher dialog and opens the sections at fault.
async saveVoucher(ev), line 5660. The form submit handler. Does one thing of its own, which is to refuse a second save while the first is still running, then hands off to _saveVoucherBody.
async _saveVoucherBody(), line 5671. The save itself, run one at a time by saveVoucher above.
rememberKingdom(v), line 5784. Kingdom belongs to the sitting, not to the specimen.
lastKingdom(), line 5788. The last kingdom used, or Plantae.
resetVoucherForm(), line 5793. Clears the form for the next voucher, keeping sitting-level values.
async clearVoucherDraft(skipConfirm), line 5830. Discards the draft and its photos, after a confirm if needed.
3.16 Autofill spill guard (lines 5845 to 5982)
_suppressBrowserAutofill(root), line 5860. Stops the browser's own address autofill from cross-wiring the form.
spillGuardedIds(), line 5897. The field ids the autofill spill guard watches, as a live list.
_spillSnapshot(), line 5912. The guarded fields' values before autofill can touch them.
_installAutofillSpillGuard(), line 5918. Installs the guard that undoes browser autofill spilling into other fields.
4.1 Render list, search, sort, bulk select (lines 5983 to 6189)
onVoucherSearchInput(), line 5990. Search box typed: filter the saved list.
onVoucherSortChange(), line 5996. Sort menu changed: re-sort the saved list.
toggleSelectAllVouchers(checked), line 6002. Ticks or clears every voucher currently shown.
toggleVoucherSelected(id, checked), line 6009. Ticks or clears one voucher.
updateBulkControls(), line 6014. Enables the bulk buttons and shows how many are ticked.
bulkDeleteSelected(), line 6029. Deletes every ticked voucher, with their photos and addenda.
renderVouchers(), line 6047. Schedules a list redraw 80 ms out, so a burst of changes draws once.
_renderVouchersNow(), line 6052. Draws the saved-voucher list now: filter, sort, cards.
async renderCardPhotos(voucherIdVal, slot), line 6159. Fills one card's photo strip.
4.2 Addendum UI (lines 6190 to 6438)
_addendaBadgeHTML(a), line 6197. The badge that says which device filed an addendum.
_addendumSummaryHTML(a), line 6206. One line describing an addendum's content.
renderAddendaPanelHTML(occurrenceID), line 6225. The addenda panel shown on a saved voucher's card.
_addendumPersonDefault(voucherId), line 6256. Who an addendum should be attributed to before anyone edits it, in the order of preference set out above.
_rememberAddendumPerson(first, last, orcid), line 6268. Remembers who filed the last addendum, for next time.
showAddAddendumModal(voucherId, occurrenceID, type), line 6274. Opens the add-addendum dialog for one voucher.
closeAddendumModal(), line 6298. Closes the add-addendum dialog.
_addendumFormatProblems(type, payload), line 6310. The save gate learned to check coordinates and dates in 108; this door never did, and it's the worse of the two.
submitAddendumModal(), line 6325. Checks and saves the addendum from the dialog.
deleteVoucher(id), line 6387. Deletes one voucher, its photos and addenda, with Undo.
openClearAllDialog(), line 6400. Opens the Clear all dialog.
closeClearAllDialog(), line 6402. Closes the Clear all dialog.
async confirmClearAll(), line 6412. Clears every voucher, addendum and photo, and redraws only after the photo sweep has finished.
4.3 Addendum problems note (lines 6439 to 6471)
renderAddendumProblems(), line 6450. findLocalAddendumProblems() has existed since the addenda went in and nothing ever called it outside the Compiler Drawer, so the local report could hold an orphan or two disagreeing redeterminations and show no sign of either.
5.1 Download helpers (lines 6472 to 6496)
downloadBlob(name, content, mime), line 6474. Saves content to a file with the workspace-prefixed name.
downloadCSV(name, csv), line 6484. Saves CSV text (with a BOM so Excel reads UTF-8).
downloadJSON(name, obj), line 6486. Saves an object as pretty-printed JSON.
downloadHTMLFile(name, html), line 6488. Saves an HTML string as a file.
recordsFor(ids), line 6491. The saved vouchers with these IDs, or all of them.
5.2 CSV export and import (lines 6497 to 6598)
_csvFormulaSafe(v), line 6515. A cell that starts with = + @ or a tab is a FORMULA to Excel, LibreOffice and Sheets, not text: they evaluate it on open, and =cmd|'/c ...'!A1 is a live command.
csvUnescapeFormulaGuard(v), line 6522. Removes the formula guard quote added on export.
csvEscape(v), line 6527. One CSV cell: formula-guarded and quoted when needed.
toCSV(records), line 6533. Records as CSV text.
exportCSV(ids), line 6540. Exports vouchers as CSV.
parseCSV(text), line 6549. Parses CSV text into rows of cells.
importCSV(ev), line 6569. Imports vouchers from a CSV file.
5.3 JSON export and import (lines 6599 to 6710)
async recordsWithPhotoManifest(recs), line 6606. Photo files travel separately via Export All Photos (ZIP); this JSON carries a manifest only.
async exportJSON(ids), line 6692. Exports vouchers as JSON, with a photo manifest.
importJSON(ev), line 6699. Imports vouchers from a JSON file.
5.4 ZIP and XLSX export (lines 6711 to 6936)
crc32(bytes), line 6717. CRC-32 checksum of bytes, for ZIP entries.
_u16(n), line 6727. A 16-bit number as two little-endian bytes.
_u32(n), line 6729. A 32-bit number as four little-endian bytes.
strToBytes(s), line 6731. Text as UTF-8 bytes.
base64ToBytes(b64), line 6733. Base64 text as bytes.
colLetter(n), line 6740. A column number as a spreadsheet letter (1 = A, 27 = AA).
xmlEsc(s), line 6773. Escapes text for XML, keeping line breaks.
makeZip(files), line 6782. Packs files into an uncompressed ZIP (used for XLSX and photo exports).
buildXLSX(records), line 6824. Builds an XLSX workbook (with QR images) from records.
exportXLSX(ids), line 6923. Exports vouchers as XLSX.
5.5 Photo ZIP export (lines 6937 to 7009)
async exportAllPhotosZip(ids), line 6953. Exports every photo for these vouchers as one ZIP, with a manifest.
5.5b Keep a copy of this file (lines 7010 to 7130)
_downloadFolderHint(), line 7041. Names this device's usual download folder, for the notice.
saveThisFileToDevice(), line 7059. Saves a copy of this report file itself to the device.
async _finishSaveToDevice(bytes, picked, name), line 7085. Waits for the bytes and the folder choice, then writes the copy.
5.5b Take it home (lines 7131 to 7712)
async _sha256Hex(bytes), line 7177. SHA-256 of some bytes as lowercase hex, or "" where the browser offers no crypto.subtle.
makeZipBlob(entries), line 7189. A stored (uncompressed) ZIP as a Blob. Same layout and UTF-8 name flag as makeZip(), but each entry's data may be a Blob that is never copied: the photo's own Blob goes into the archive by reference, so a 200 MB part never needs 200 MB of bytes in memory at once.
async _bundleTextEntry(name, text), line 7210. One text file as a ZIP entry, with its crc, size and SHA-256.
_bundledPhotoMap(), line 7216. Photo id to the name of a saved or shared bundle that holds it.
async _bundlePlan(recs, limits, opts), line 7223. Reads which photos each voucher has and cuts the vouchers into parts.
_bundleCard(rec, photoPaths), line 7253. One voucher as the viewer page shows it. Names come through _csPrepare() and _odsNameHTML(), the same reader every other export uses, so the page and the files beside it cannot disagree on the current name.
_bundleViewerHTML(meta, cards), line 7276. The open-me.html page. Everything it shows is embedded, so it opens from a folder with no server and no network.
_bundleReadme(meta, fileCount), line 7339. README.txt for one part: how to open it and how to check it.
async _bundleBuildPart(plan, k, bundle), line 7375. Builds one part: reads each photo once (for its crc and SHA-256), then hands the photo's own Blob to the ZIP.
_takeHomeSettings(), line 7442. Where bundles are going, remembered per workspace: route ("device" or "github"), and for GitHub the owner/repo and branch.
_githubRepoClean(v), line 7447. Owner/repo as GitHub allows it, or "" when it is not one.
_githubUploadURL(repo, branch), line 7452. The repo's upload page, pointed at the take-home folder on that branch.
async openTakeHomeModal(ids, opts), line 7457. Opens the Take it home dialog for these vouchers, or all of them.
async _takeHomeSetNewOnly(on), line 7470. The "only photos not taken home yet" box.
async _takeHomeReplan(), line 7477. Cuts the parts for the chosen route and redraws.
async _takeHomeSetting(key, value), line 7485. Saves a change to the route, repo or branch and replans.
_takeHomeGithubNote(), line 7495. The GitHub line: the upload link once the repo is valid, and what to do there.
_takeHomeRender(), line 7507. Draws the dialog from _takeHome.
closeTakeHomeModal(), line 7555. Closes the Take it home dialog and lets go of any built ZIPs.
async _takeHomePrepare(k), line 7562. Builds one part, logs what went into it, then offers Share and Save.
_takeHomeHandedOff(k, how), line 7589. Marks a part as having left the device, in the log the Remove step reads.
async _takeHomeShare(k), line 7595. Sends a prepared part through the device's share sheet.
_takeHomeSave(k), line 7610. Downloads a prepared part.
async clearBundledPhotos(), line 7629. The Remove step. Walks every saved voucher; a voucher's photos go only if every one of them is in a bundle that was saved or shared and still hashes to what that bundle recorded.
5.5c Bring it back, and what is not safe yet (lines 7713 to 7960)
async _putPhotoRecord(rec), line 7727. Writes one photo row straight into the photo store, keeping its original id.
async importBundleFiles(ev), line 7736. The Import bundle button: every picked ZIP, in name order.
async _importOneBundle(file, total), line 7758. One take-home ZIP: records in through addRecords(), then each photo checked and re-attached to the voucher as saved here.
scheduleTakeHomeNote(), line 7849. Schedules a refresh of the not-yet-taken-home line.
async renderTakeHomeNote(), line 7854. Counts vouchers with photos in no handed-off bundle, and draws the line.
_anyDialogOpen(), line 7908. True when some other dialog is already up, so the offer waits its turn.
async maybeOfferSnapshot(), line 7913. Decides whether to offer a snapshot now, and offers it.
_snapshotSnooze(hours), line 7947. Closes the offer and holds off for this many hours.
_snapshotYes(), line 7953. Yes: close the offer, open the snapshot. The short snooze keeps the offer from coming straight back if the snapshot is then abandoned.
5.6 Email (lines 7961 to 7997)
async emailreport(ids), line 7969. Shares or emails the JSON export.
5.7 HTML table export (lines 7998 to 8030)
plainDoc(title, inner, extraCSS), line 8000. A plain printable HTML page around some content.
exportHTMLTable(ids), line 8018. Exports vouchers as a printable HTML table.
5.8 Print labels (lines 8031 to 8178)
_taxonHTML(name, qualifier), line 8041. A determination as label HTML: name words italic, qualifier and rank connectors roman.
labelInner(v), line 8058. The inside of one herbarium label.
labelDoc(records), line 8107. A page of herbarium labels, four by three inches.
printHTMLNow(html), line 8139. Opens HTML in a new window and prints it.
printLabels(ids), line 8173. Opens herbarium labels for printing: the current determination on top, the original kept as Orig. det.
5.9 QR sheets (preprint) (lines 8179 to 8371)
_voucherIdTail(id), line 8197. Last four of a Voucher ID, lowercase; "" if too short.
_setVoucherIdLabel(el, id), line 8202. Writes a Voucher ID into the draft QR panel with its last four in bold.
_qrSheetDefaultRows(), line 8209. The sheet's lines as [{label, value}], remembered in uiState.
_qrSheetRows(), line 8217. The saved QR sheet lines, or the defaults.
_qrSheetRowsFromDOM(), line 8225. Reads the row editor back out of the dialog; empty rows drop.
_qrSheetSave(), line 8232. Saves as you type, so Cancel still remembers.
_qrSheetRenderRows(rows), line 8234. Draws the row editor: label, value, remove.
_qrSheetAddRow(), line 8244. Adds an empty line and puts the cursor in its label.
_mintSheetIds(n), line 8255. Mints n lowercase v4 UUIDs whose last four are unique within the batch and don't repeat any last four already on a voucher in this browser (or the current draft).
qrSheetDoc(ids, fields), line 8273. Builds the print window: one letter page per UUID.
openQrSheetModal(), line 8310. Opens the QR sheets dialog, building it on first use.
closeQrSheetModal(), line 8344. Closes the QR sheets dialog.
_qrSheetKeydown(e), line 8351. Escape closes the dialog.
_qrSheetSetCount(n), line 8353. Picks 10/20/50/100 and lights the matching button.
_qrSheetResetFields(), line 8358. Back to the default six lines, names from the primary collector.
printQrSheets(), line 8363. Saves the lines, mints the batch, opens the print window.
5.10 QR tags (reconciliation print) (lines 8372 to 8624)
_qrTagPerPage(), line 8412. The saved tags-per-page choice, or 12.
_qrTagSetPerPage(n), line 8418. Picks 20/12/6 per page, lights the button, remembers it.
_qrTagNameHTML(v), line 8425. The name line for a tag: qualifier upright, binomial in italics.
_qrTagTime(v), line 8434. 24-hour local time the draft was started (falls back to save time).
_qrTagDates(), line 8440. Distinct collection dates with counts, newest first.
_qrTagRecords(scope), line 8446. The records a scope names, oldest saved first (field order).
_qrTagRenderScopes(), line 8453. Fills the scope picker: selected (if any), each day, all.
_qrTagRenderList(), line 8464. Draws the checklist for the current scope; sheet-ID vouchers start unticked.
_qrTagToggle(cb), line 8480. One checkbox in or out of the print.
_qrTagSetAll(on), line 8485. All / None for the checklist.
_qrTagCount(), line 8489. Keeps the Print button honest about how many tags it makes.
_qrTagSetScope(v), line 8495. Scope picker changed.
qrTagDoc(recs, perPage), line 8498. Builds the print window: a letter-page grid of cut-apart tags, grouped under a collector and date heading.
openQrTagModal(), line 8560. Opens the QR tags dialog, building it on first use.
closeQrTagModal(), line 8598. Closes the QR tags dialog.
_qrTagKeydown(e), line 8605. Escape closes the dialog.
printQrTags(), line 8608. Prints the ticked vouchers in save order. The print window is opened inside the click so a pop-up blocker doesn't eat it.
5.11 Collection systems: Symbiota, Specify and DiSSCo (lines 8625 to 9518)
_csTermURI(name), line 8703. The term URI for a column name: Dublin Core, Symbiota, or Darwin Core.
_csCell(v), line 8709. One CSV cell for a machine file: control characters dropped, RFC 4180 quoting, nothing else.
_csCSV(headers, rows), line 8715. A header and rows of objects as CSV text, LF line ends, trailing newline.
_csTidy(v), line 8721. Trims and collapses runs of whitespace.
_csFull(first, last), line 8723. "First Last" from two parts, skipping blanks.
_csParseName(raw), line 8729. Splits a scientific name into genus, epithet, rank, infraspecific epithet and authorship.
_csMeters(raw), line 8764. Reads "±4365 m", "10m", "0.5 km", "30 ft", "0.5 mi" as whole metres.
_csQualifier(raw), line 8778. Ironfist's qualifier as a collection system writes it. determined is no qualifier.
_csUSState(raw), line 8788. A U.S. state or territory by postal code or full name. { value, matched }.
_csOrcid(raw), line 8800. A bare ORCID iD as its https://orcid.org/ URI; anything else passes through.
_csSplitCollectors(raw), line 8807. additionalCollectors ("A B | C D") as [{ first, last, guess }].
_csPrepare(recIn), line 8820. The one reader. Every derived value both exports use, worked out once from a decorated record (addenda and current_* fields on it), plus the review flags.
_csReviewRows(P, system), line 8970. review.csv rows for one prepared record, only the flags that apply to this system.
_csReview(rows), line 8976. review.csv text, sorted fix, then check, then info; with the counts.
_csSymbiotaOcc(P), line 8988. The occurrence core row for one prepared record, plus any added-term conflicts as flags.
_csSymbiotaDets(P), line 9021. Identification extension rows for one prepared record, oldest first.
_csMetaXml(occCols, detCols), line 9030. meta.xml for the archive: core and extension, one field element per column after the key.
buildSymbiotaArchive(recs), line 9045. The Symbiota ZIP entries ([{ name, text }]) for these decorated records, and the review counts.
_csSpecifyTaxonCols(n, h, family), line 9096. One determination's WorkBench columns as [header, value] pairs. n is 1 for the current one (no prefix, and Family), 2 and up for earlier ones ("Det 2 ...").
_csSpecifyRow(P, nColl, nDet, addedTerms), line 9117. One WorkBench row as [header, value] pairs, sized to the most collectors and determinations any exported record has, so every row has the same columns.
_csSpecifyTarget(header), line 9145. Where a WorkBench column goes in Specify's stock schema: { table, field, note }.
buildSpecifyWorkbench(recs), line 9208. The Specify ZIP entries ([{ name, text }]) for these decorated records, and the review counts.
_odsClean(v), line 9301. Drops empty strings, nulls, empty arrays and objects left holding nothing but their @type, so the file carries no hollow keys.
_odsNum(s), line 9313. A decimal string as a number, or undefined when it isn't one.
_odsAgent(first, last, orcid, role, position), line 9315. One person as an openDS agent with a single role.
_odsNameHTML(n), line 9325. ods:scientificNameHTMLLabel: genus and epithets italic, the × outside the italics, rank marker and authorship roman.
_odsSpecimen(P), line 9340. One prepared record as an ods:DigitalSpecimen.
buildOpenDS(recs), line 9430. The openDS ZIP entries ([{ name, text }]) for these decorated records, and the review counts.
_csDownload(built, base, label), line 9488. Zips a builder's entries and downloads them, then reports the review counts.
async exportSymbiota(ids), line 9496. Export for Symbiota: a Darwin Core Archive ZIP of these vouchers, or all of them.
async exportOpenDS(ids), line 9502. Export for DiSSCo: an openDS Digital Specimen ZIP of these vouchers, or all of them.
async exportSpecify(ids), line 9508. Export for Specify: a WorkBench data set ZIP of these vouchers, or all of them.
6.1 Toasts (lines 9519 to 9563)
showUndoToast(message, undoFn), line 9531. Shows a message with an Undo button for a few seconds.
hideUndoToast(), line 9541. Hides the Undo message.
runUndo(), line 9548. Runs the pending undo, then hides the message.
showNotice(message, isError), line 9554. Shows a short message; errors stay up longer.
6.2 Section state (lines 9564 to 9577)
initSectionPersistence(), line 9568. Remembers which form sections are open or closed.
6.3 Theme (lines 9578 to 9593)
toggleTheme(), line 9580. Switches light and dark theme and remembers it.
initTheme(), line 9587. Applies the saved theme, or the system's.
6.4 Build name (lines 9594 to 9608)
buildLabel(), line 9600. The short form the header chip wears, I.F.<n>.
applyBuildName(), line 9602. Puts the build name in the tab title and the header stamp.
6.5 Workspace bar (lines 9609 to 9728)
_wsHue(name), line 9619. A stable hue from the workspace name. I just know this is gonna bite me in the arse.
_wsColor(name), line 9625. The workspace's color, or none for the default.
_wsListLoad(), line 9627. The shared list of workspaces this browser has opened.
_wsListRemember(name), line 9632. Adds a workspace to the shared list.
_wsHref(name), line 9641. URL for a workspace: this same file, ?ws=name (none for default).
_wsFavicon(), line 9643. Tab icon: the workspace's first letter on its color.
renderWorkspaceBar(), line 9657. Paints the bar, the stripe and the icon for this workspace.
_wsRenderList(), line 9673. The switcher list: default plus every named workspace, as plain links.
toggleWorkspacePanel(force), line 9683. Opens or closes the workspace switcher.
confirmWorkspaceSwitch(name), line 9693. New workspace: tidy the name, remember it, go there in this tab.
guardWorkspaceLink(e), line 9705. Intercepts a click on a workspace link so the draft is flushed before the navigation takes the page away.
openNewWorkspace(), line 9715. Reads the name box, normalises it to the lowercase-hyphen shape workspace names are limited to, and opens that workspace.
6.6 Same workspace, two tabs (lines 9729 to 9775)
_wsBeat(), line 9744. Tells other tabs this workspace is open here.
_wsWarnTwice(), line 9746. Warns that this workspace is also open in another tab.
initWorkspacePresence(), line 9753. Starts the two-tabs check.
6.7 Session badge and top stamp (lines 9776 to 9823)
renderSessionBadge(), line 9785. Shows the schema version and short device tag.
renderTopStamp(), line 9793. Shows when this page was opened and the short device tag.
async copySessionIdToClipboard(elId), line 9801. Copies the full device tag to the clipboard.
7.1 Storage resilience (lines 9824 to 9991)
_persistAsked(), line 9884. Has this browser already been asked for persistent storage?
_markPersistAsked(), line 9890. Notes that this browser has now actually answered the ask.
requestPersistNow(), line 9895. The storage strip's button: asks this browser to keep the data.
_persistWithTimeout(), line 9910. Resolves to null if the browser prompt goes unanswered, so nothing here hangs on it.
async checkStorageResilience(opts), line 9917. Checks how safe local storage is here and shows a note if it is at risk.
7.2 Store reconciliation (lines 9992 to 10185)
async reconcileStores(), line 10024. Compares saved vouchers against stored photos and reports orphans either way.
async purgeOrphanPhotos(), line 10127. Deletes every photo with no voucher behind it, after confirming.
async exportOrphanPhotos(), line 10151. Saves the orphaned photos to a ZIP before anyone deletes them.
7.3 Runtime tracer (?trace) (lines 10186 to 10300)
_traceOn(), line 10220. True when the URL asks for ?trace.
_traceWrap(name, fn), line 10225. Wraps a function so the tracer counts its calls and callers.
_traceModule(moduleName, fns), line 10244. Wraps every function in a module object for the tracer.
_traceInstall(), line 10251. Turns the tracer on for every global function (only with ?trace).
_traceReport(), line 10283. What the tracer has seen so far.
_traceDump(), line 10293. Prints the tracer report to the console.
7.4 Self test (?selftest) (lines 10301 to 10612)
async runSelfTest(), line 10318. The ?selftest round trip: one fixture voucher through store, exports and back.
mapTableForSelfTest(headers, rows), line 10595. Maps a parsed table's columns to FIELD_DEFS, for the self test's CSV re-import.
9.2 Compiler drawer: merge rules (lines 10959 to 11082)
withCompileBatchId(recs), line 10967. Stamps this compile's batch id on each record.
mergeAddendaInto(existing, incoming), line 10979. Folds a second copy's addenda into the kept record, deduplicated by addendum id, so a duplicate occurrenceID no longer loses its corrections.
mergePhotoLists(target, winnerPhotos, otherPhotos), line 11005. Union of two photo manifests for one specimen.
_stampMs(v), line 11026. A timestamp as ms, or null.
mergeFieldsInto(existing, incoming, incomingStamp), line 11039. The field half of a same-occurrenceID merge: the newer copy's values win, photo lists are unioned, and the copy set aside is recorded as a conflict.
9.3 Compiler drawer: compile and display (lines 11083 to 11241)
el(id), line 11085. Shorthand for document.getElementById inside the drawer.
recordKey(r), line 11095. The merge key for a record: its occurrenceID, or its Voucher ID when it has none.
mergeRecords(items, sourceLabel), line 11101. Merges one source's records into the compiled set, counting what changed.
cleanRecords(), line 11146. The compiled records without the drawer's own bookkeeping fields.
collectorsCount(), line 11154. How many distinct collectors are in the compiled set.
photoRefNames(), line 11163. Every photo filename the compiled records point at.
fingerprintDrift(), line 11178. The provenance question a reviewer actually asks about a drawer that holds its own private copy of everything: can what it exports disagree with the vouchers it read?
render(), line 11189. Redraws the drawer's counts, messages and conflict list.
9.4 Compiler drawer: readers (lines 11242 to 11466)
mapTable(headers, rows), line 11244. Maps table columns to FIELD_DEFS and returns records.
parseJsonText(text), line 11264. Voucher records from JSON text.
parseCsvText(text), line 11271. Voucher records from CSV text.
parseHtmlText(text), line 11277. Voucher records from an exported HTML table.
async _inflateRaw(bytes, expected), line 11300. Inflates one deflated ZIP entry, refusing a runaway one.
async readStoredZip(buffer), line 11321. Reads an uncompressed ZIP into {path: bytes}.
colIndex(ref), line 11364. A spreadsheet cell reference's column as a 0-based index.
_xmlDoc(bytes, what), line 11370. Parses XML bytes, with a clear error if broken.
_firstSheetPath(entries), line 11383. The path of the first worksheet in workbook order.
_excelSerialToISO(v), line 11404. An Excel date serial as YYYY-MM-DD; other values pass through.
async parseXlsxBuffer(buffer), line 11411. Voucher records from an XLSX file.
async parsePhotoZip(buffer), line 11447. Loads photos and their manifest from a photo ZIP.
9.5 Compiler drawer: file intake (lines 11467 to 11532)
async processFile(file), line 11469. Reads one dropped file by type: JSON, CSV, HTML, XLSX or photo ZIP.
async loadFiles(files), line 11493. Reads every dropped file in turn, reporting failures without stopping.
async addThisreport(), line 11509. THE SEAM. This function is the only place in this closure that reads anything belonging to the rest of the file: load() for the saved vouchers, getPhotosForVoucher() for their images.
9.6 Compiler drawer: exports (lines 11533 to 11612)
saveBlob(blob, filename), line 11535. Saves a Blob with the workspace-prefixed name.
exportCompilerJSON(), line 11542. Exports the compiled set as JSON.
exportCompilerCSV(), line 11553. Exports the compiled set as CSV.
exportCompilerHTML(), line 11558. Exports the compiled set as an HTML table.
exportCompilerXLSX(), line 11569. Exports the compiled set as XLSX.
exportCompilerPhotos(), line 11578. Exports the compiled set's photos as one ZIP.
clearCompiler(), line 11607. Empties the drawer and starts a new batch id.
10 Pane shell (lines 11692 to 11937)
isNarrow(), line 11712. True while the layout shows one pane at a time.
currentPane(), line 11715. The pane showing on a phone: "1", "2" or "3".
showPane(n), line 11718. Shows one pane on a phone. On a wide screen it just makes sure that pane is open.
toggleSide(side), line 11729. The wedges. Narrow: swap between that pane and pane 2.
paint(), line 11742. Keeps the tab states and the wedge arrows honest.
revealElement(el), line 11767. Brings an element into view wherever it is parked: switches to its pane on a phone, reopens a collapsed pane on a wide screen, opens every <details> above it, then scrolls its own pane.
syncIdentificationChoices(), line 11858. Pushes the current <select> values onto the card-style choice triggers that stand in front of them.
close(), line 11882. Shuts the choice dialog. Named so the three ways out, the Close button, a click on the backdrop and Escape, all go through one path.
B.3 Notable engineering decisions, with location
Each item is a change made after real use exposed a failure, unless it says otherwise. The line is where the rationale sits in the source.
Line 1633. Storage writes fail loudly. A refused localStorage write used to be swallowed and the cache reported the record as saved. The cache is still kept, since the record is real and exportable, but a persistent banner says the disk did not take it.
Line 1709. Two tabs, one store. Every write re-reads disk first, and mutate() puts each read-modify-write between one fresh read and one write, so a second tab's save is no longer overwritten by a stale copy.
Line 1769. One failure reporter for two stores. Record store and draft autosave write different keys but fail the same way; one function owns the message and the banner.
Line 1829. Local calendar date. todayISO() uses local date parts, not toISOString(), so evening records are not dated tomorrow.
Line 1949. Addenda instead of overwrites. Corrections get their own rows keyed to the immutable occurrenceID.
Line 2086. Addenda die with their voucher. Deleting a record used to orphan its corrections, which rode into every export and surfaced months later in a compile with nothing to attach to.
Line 2136. One addenda parser for every format. Nested array or serialized cell, same function, mangled cells tolerated.
Line 2259. Georeferences by supersession, not position. After a compile, array order is load order, so latest-wins let one of two independent corrections win silently. Each georeference now records what it supersedes; more than one standing head is reported as a conflict.
Line 2316. Local addendum problems are reported. The check existed only in the compiler; it now runs on the local set and reports without deciding.
Line 2382. Addenda survive import. Export nested the chain; import ran rows through FIELD_DEFS and discarded the nested array. Now every incoming addendum is upserted, and parent pointers are rewritten to the record as saved locally.
Line 2630. Local import dedupes on occurrenceID. A colleague's copy with a different Voucher ID and the same occurrenceID used to import as a second specimen. It now folds into the existing one.
Line 2776. occurrenceID survives relabeling. Scanning a preprint sheet used to clobber the only UUID the record had. The specimen UUID is now minted at draft start and never touched by ID reassignment.
Line 2897. URL-shaped IDs migrated with backup. A separate backup key is written once before any record is altered.
Line 3206, 4056. Draft resync after programmatic writes. GPS and EXIF write .value directly, which fires no input event; without an explicit resync the save gate saw stale blanks.
Line 3288. Safari IndexedDB Blob caveat, stated and left. The known WebKit Blob bug is documented with the ArrayBuffer workaround named, and not applied blind without hardware to test on.
Line 3304. A failed photo store open is not cached. One refusal used to fail every later photo call until a reload, even after space was freed. The rejection now clears the cache, so the next call tries again.
Line 3482. Voucher ID in EXIF. Hand-built APP1 segment; no library.
Line 3614. A photo keeps its first filename. Filenames used to be positions in the export list, so a second take-home bundle could reuse a name and confuse two photos on import. A photo now keeps the first name it went out under.
Line 4130. Timer arms on typing, scoped to the draft. Button-only arming logged hand-typed records as zero seconds; page-wide arming let export clicks start the clock on an empty draft.
Line 4190. Det mirrors collector until hand-edited. Tracks the last auto-filled value to distinguish an edit from a default.
Line 5008. Site banked at save. Duplicate-for-this-site read the live form, which was blank in the one sequence everyone uses (save, then duplicate).
Line 5120. Autosave carries identity. Restoring prose under freshly minted IDs separated the draft from its photos and its preprinted sheet. The whole draft, IDs included, is now saved and restored.
Line 5183. Pristine snapshot instead of empty test. A reset form is never empty (date, kingdom, datum, institution carry over), so "has content" compares against a snapshot of the reset state.
Line 5532. Format gate on dates and coordinates. Non-empty is not a coordinate. Shape is validated; plausibility is not claimed.
Line 5588. Photo and no-photo are mutually exclusive. The checkbox used to short-circuit the whole check, so a voucher could carry an image and a "no photo" flag at once. The gate now refuses the contradiction.
Line 5787. Kingdom belongs to the sitting. Persisted across reset and reload; defaults to Plantae only on first run.
Line 6057. List decorated with current state. Search, sort, and card headline agree with export and label.
Line 6302. Addendum dialog gets the same format gate. A bad corrected latitude would become current_latitude, the column downstream readers trust most.
Line 6498. Spreadsheet formula guard. Apostrophe prefix on cells starting with = + @ or tab; numeric values exempt so longitudes survive; reversed on import.
Line 6691. JSON carries a photo manifest, not bytes. Keeps the export small and diffable; the ZIP and the take-home bundle carry the images under matching names.
Line 7131. Records and photos leave together. They used to leave as two files joined only by a naming convention. Take it home writes one ZIP per part with both, a page that shows them together, and a SHA-256 for every file. A voucher is never split across two parts.
Line 7167. Parts cut to GitHub's limits. GitHub's browser upload takes no file over 25 MiB and no more than 100 files at once, so the GitHub route cuts parts at 24 MiB and 90 photos. The file builds the part and the link; the commit is the collector's.
Line 7625. Photos are removed only when provably safe. Freeing space deletes a voucher's photos only if every one is in a saved or shared bundle and still hashes to what that bundle recorded.
Line 7713. Bundles come back checked. Take it home was one-way. Import bundle puts records back through the single import door and checks each photo against its manifest hash; a photo that fails is named, not attached.
Line 7912. A snapshot offered, never forced. Some browsers clear site data without warning. When unbundled work builds up, the file asks once whether to make a copy. Nothing is saved without a tap.
Line 8145. Labels print the current determination. A decision rather than a repair. The original determination stays on the label as "Orig. det." with redeterminer and date.
Line 8817. One reader for three collection exports. Symbiota, Specify, and DiSSCo read each record through _csPrepare(), so they agree on the current name, collectors, coordinates, and review flags.
Line 9268. DiSSCo keys left out, not faked. Seven required openDS keys are minted by DiSSCo at ingest. The export omits them and its README says so; the file is a pre-ingest Digital Specimen.
Line 9838. WebKit eviction, both mechanisms. Storage-pressure eviction (mitigated by persist()) and ITP's 7-day rule (mitigated only by Home Screen install) are distinguished, and the advisory is labeled untested.
Line 10971. Compiler merges addenda on duplicate occurrenceID. The second copy's corrections were previously discarded with the copy.
Line 11028. Compiler merges fields, newer wins. The first copy loaded used to win outright, so a later, corrected export lost to an older one. The newer copy's values now win, photo lists are unioned, and the copy set aside is named.
Line 11086. Compiler uses the single entry door. The drawer used to carry its own near-copy of canonicalRecord(); flat formats compiled to an empty chain, and a CSV round trip reverted redeterminations.
Line 11609. Fresh compile batch id on clear. A batch identity ends when the drawer is cleared.
B.4 SERNEC retrieval doctrine and sampling script
SERNEC agent retrieval doctrine, v2 candidate freeze
Purpose
This is a bounded specimen-data retrieval study. The agent is a retrieval worker, not a taxonomist, data cleaner, analyst, or study designer.
THE AGENT'S JOB IS RETRIEVAL, NOT JUDGMENT.
Failure is data. Substitution is contamination.
Source boundary
- Use SERNEC only for specimen retrieval: https://sernecportal.org/portal/collections/search/index.php
- Search the exact supplied collection/institution code and genus.
- Do not rescue a failed SERNEC search with GBIF, iDigBio, Google, an institutional portal, NCBI, or another source.
- The SERNEC interface searches a shared SEINet database, so selecting the exact frozen collection code is mandatory.
Frozen study arms
A. RANDOM_SURVEY
Micranthes; Potentilla; Euphorbia; Ribes; Ranunculus; Lupinus; Polygonum; Solidago; Chorizanthe; Eragrostis.
B. STRESS_TEST, a separate deliberate study
Carex; Cyperus; Solidago; Panicum.
The stress-test genera were deliberately selected and are not part of the random survey inference.
Solidago occurs in both arms by design. RANDOM_SURVEY/Solidago and STRESS_TEST/Solidago are separate experimental cells. Do not merge them.
Frozen institution panel
The source list contains 486 collection entries and was already randomized with seed 20260827. Restrict that frozen list to rows tagged SERNEC - Southeastern Herbaria, preserve its existing random order, and take the first 20. The resulting 20 are fixed in 01_FROZEN_sernec_institutions_20.csv.
Do not redraw, substitute, or replace an institution because it is sparse, empty, broken, awkward, or unexpectedly large.
Retrieval unit
The retrieval unit is:
Study_Arm × Institution × Genus
There are 280 frozen retrieval cells: 20 institutions × (10 random genera + 4 stress-test genera).
Species are NOT preselected in this phase. A previous draft introduced an unsupported target of 10 species per genus; that rule is withdrawn. Species names are retained exactly as they occur in the specimen records returned by SERNEC and may be analyzed later as a separate, explicitly designed step.
Record sampling rule
Target = up to 100 specimen records per retrieval cell.
For each Study_Arm × Institution × Genus:
- Search SERNEC for the exact frozen institution/collection and genus.
- Record the result count reported by SERNEC when available.
- Export the complete matching result set whenever SERNEC permits it.
- Preserve the raw export unchanged.
- If the complete result contains 0 records: retain/report 0.
- If it contains 1–100 records: retain all records.
- If it contains >100 records: run the supplied Python sampler on the complete raw CSV and retain a reproducible random sample of 100 rows without replacement.
- Never use the first 100 displayed records as a substitute for random sampling.
- If SERNEC reports more matches than it allows the agent to export, mark the cell
CAPPED; do not pretend a random sample from a truncated export represents the full cell.
Species handling
- Do not choose, balance, replace, normalize, or stratify species during retrieval.
- Do not force equal numbers of species.
- Preserve whatever species determinations occur in the sampled specimen records.
- Blank,
sp., cf., aff., infraspecific, synonymized, or odd taxon strings remain untouched. - Any later species-level sampling is a separate study decision and must be frozen before it is performed.
Preservation rules
Do not clean, normalize, correct, infer, georeference, deduplicate, merge, enrich, or taxonomically reconcile raw SERNEC records.
Never overwrite a raw export. Derived 100-row samples must use a distinct filename.
Suggested raw filename: <StudyArm>__<InstitutionCode>__<Genus>__RAW.csv
Suggested sampled filename: <StudyArm>__<InstitutionCode>__<Genus>__SAMPLE100.csv
Stop / failure rules
Stop and report rather than improvise if:
- no matching SERNEC records;
- portal result/download cap prevents complete export;
- timeout/hang;
- institution/collection filter appears wrong;
- genus query is ambiguous;
- CSV download fails;
- supplied URL is unavailable;
- requested collection cannot be found.
Do not substitute another genus, institution, portal, query interpretation, or source.
Required return for every cell
- Job_ID
- Study_Arm
- Institution_Code
- Institution_Name
- Genus
- Record_Target
- SERNEC_Result_Count (if displayed)
- Exported_Row_Count
- Retrieval_Status: COMPLETE / SPARSE / ZERO / FAILED / CAPPED
- Query_or_Result_URL
- Retrieval_Date
- Raw_Filename
- Failure_or_Note
One-sentence operating rule
Search exactly what you were given, export exactly what SERNEC returns, preserve it exactly, and report every failure instead of fixing it.
Sampling script
Caveat: the loop was reversed on the fly during the run. That change is not represented in the listing below.
#!/usr/bin/env python3
"""Reproducibly sample up to 100 specimen rows from one COMPLETE raw SERNEC CSV.
Usage:
python 04_sample_records.py RAW.csv SAMPLE.csv STUDY_ARM INSTITUTION_CODE GENUS
Rules:
- RAW.csv is never modified.
- 0..100 data rows -> retain all.
- >100 data rows -> sample 100 without replacement.
- Sampling seed is deterministically derived from MASTER_SEED + cell identity.
- Output preserves the original column set. Selected rows are written in original
raw-file order after row indices are sampled.
"""
import csv
import hashlib
import random
import sys
from pathlib import Path
MASTER_SEED = 20260828
TARGET = 100
if len(sys.argv) != 6:
raise SystemExit(
"Usage: python 04_sample_records.py RAW.csv SAMPLE.csv STUDY_ARM INSTITUTION_CODE GENUS"
)
raw_path = Path(sys.argv[1])
out_path = Path(sys.argv[2])
arm = sys.argv[3].strip()
institution = sys.argv[4].strip()
genus = sys.argv[5].strip()
raw_bytes = raw_path.read_bytes()
raw_sha256 = hashlib.sha256(raw_bytes).hexdigest()
with raw_path.open("r", newline="", encoding="utf-8-sig") as f:
reader = csv.reader(f)
try:
header = next(reader)
except StopIteration:
raise SystemExit("RAW CSV is empty: no header row")
rows = list(reader)
n_available = len(rows)
seed_text = f"{MASTER_SEED}|{arm}|{institution}|{genus}|{raw_sha256}"
seed_int = int(hashlib.sha256(seed_text.encode("utf-8")).hexdigest(), 16)
if n_available <= TARGET:
selected_indices = list(range(n_available))
rule = "ALL_AVAILABLE"
else:
rng = random.Random(seed_int)
selected_indices = sorted(rng.sample(range(n_available), TARGET))
rule = "RANDOM_100_WITHOUT_REPLACEMENT"
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(header)
for i in selected_indices:
writer.writerow(rows[i])
print(f"RAW_SHA256={raw_sha256}")
print(f"CELL={arm}|{institution}|{genus}")
print(f"AVAILABLE_ROWS={n_available}")
print(f"SELECTED_ROWS={len(selected_indices)}")
print(f"RULE={rule}")
print(f"MASTER_SEED={MASTER_SEED}")
print(f"OUTPUT={out_path}")