-
Notifications
You must be signed in to change notification settings - Fork 0
Import
- Left rail
$ importfield (280px rail, not route, not overlay) - Top-right
[ Sync / Import ]->Import addon (wb1!) - Settings -> Import (same decoder)
- Hotkey
ianywhere. Opens rail tab if collapsed.
- Click Import or press
i. Must be user gesture fornavigator.clipboard.readText(). - If clipboard is
wb1!length >10 -> auto-fill preview. Zero paste needed. - Otherwise text area focused "Paste wb1! here Ctrl+V" -> debounce 150ms -> preview.
- Preview sentence (rail cannot hold 8 columns): "6 characters, 1 over 3 days, Enter to import" plus rows with dots, hover per-section.
- Enter confirms ->
POST /api/addon/import{ bundle:"wb1!..." }-> D1character_addon_cacheandwarband_bank_cacheupsert peruser_id+char_key. - Receipt: one line per character dot plus gold printed in waves like
$output, held 6 seconds, decayed in 4 steps, removed. Block returns to original height. - Nothing reloads. Only that line is stamped. Reload would discard receipt (what overlay version did).
Client-side before D1 write:
- Not
wb1!-> ifwb0!then "That is Camp DNA, not inventory, use Share tab" else ifwbc1!then "That is cleanup string for addon, paste into /warband junk" else "Does not look like wb1!, run /warband copy again" - Over 20 characters or over 1 MB decoded -> "Too large, try single-character"
- Missing characters not deleted -> keeps old snapshot aging red. Staleness lowers confidence, never deletes.
- One character 39 KB JSON 8.6 KB wire, 6 154 KB / 26 KB, 20 cap 474 KB / 73 KB. 4-7 KB figure is slim with no per-item lists.
src/lib/warband-import.ts is pure, testable via Vitest, no DOM or D1:
-
WIRE_PREFIX='wb1!',DNA_PREFIX='wb0!',CLEANUP_PREFIX='wbc1!',MAX_CHARACTERS=20,MAX_DECODED_BYTES=1_048_576 - Base64url -> bytes ->
DecompressionStream('deflate-raw')notdeflate(zlib header addon does not emit, use raw orpako.inflateRaw) - Throws on invalid input.
Web tests src/__tests__/warband-import.test.ts. Addon vectors docs/contract/vectors/v1-min.wb1, v1-gear.wb1, v1-vault.wb1, wbc1-min.wbc1.
Preview is rail-width decision about a paste that has not happened. Facts are what app reads after paste.
warband-import.ts exports:
-
freshness(seenSec, nowMs)-> dot from seconds stamp. -
freshnessMs(seenMs, nowMs)-> same question from already converted milliseconds.deriveFacts()converts once so downstream holds ms only. -
freshnessVar(dot)-> token map. One map so rail receipt and roster columns agree. -
deriveFacts(CharacterObject)->AddonFactswith level, gold, free slots, keystone, vault buckets with optional rows, consumables, currencies, gear counts, andseenMsin ms. Conversion seconds to ms happens exactly once here. -
purse(facts, bankGold)-> summed character gold plus bank gold with reported count. -
currenciesNearCap(facts, {lastResetMs})-> account-wide only, deduped by id, freshest reading wins, weekly caps dropped if before last reset. -
findItems(characters, warbank, ids)-> every stack of ids by character and container. Bank counted once, hit stamped from containers actually matched.
lib/addon-cache.ts is only D1 reader: readAddonSnapshot(db, userId) -> map of CharacterObject, warbank, importedAtMs. Degrades to empty on schema drift, never throws for drift. GET /api/addon/data returns facts, never calls Blizzard, never 401s.
Facts carry no policy. Whether nine day old vault may be shown is consumer decision. tonight.ts discards vault before reset, roster dots age and shows anyway. Both correct for surface.
Since addon 1.2.0, weeklyVault buckets carry optional rows[]. Each row is {t} threshold, {p} progress, {l} level raw, {d} difficulty name for raid only. Absent from every bundle before 1.2.0, so decoder treats null as older than field, never empty array. Row with no threshold dropped. Consumer must not resolve l itself, and must not compare l across buckets. Missing d is client declining, not unknown. This lets Tonight Plan say "one more heroic boss raises slot you already have" without hardcoded [2,4,6].
Since addon 1.1.0, gear[] flat array per character, each entry {slot, where, id, ilvl, s, n, q, b, cls, sub}. Last five added 1.3.0, bag/bank/warbank only, optional. n is name because app has no item-id-to-name lookup. cls sub let site judge wearability because addon sends everything equip location allows. Sub is raw subclass, slot says whether it is armor weight. Cloak reports 1 but is not cloth.
Correction 2026-08-23: no bag/bank/warbank gear reached wire between 1.1.0 and 1.3.0. Bundles from those versions carry gear that is 100 percent where:"equipped". Site detects that pair and tells addon needs update.
previewRows() reports gearCount and ownedGearCount. Null when key absent, distinct from 0. Pair of gearCount non-zero and ownedGearCount 0 is signature of 1.1.0-1.2.x bug.
b absent on 1.3.0 bundle means not bound, BoE case worth money. Guard dates bundle by cls, sent unconditionally on every bag entry by 1.3.0, so presence proves dialect in which missing b is statement.
Where is my stuff is feature per-item lists exist for, and reason MAX_DECODED_BYTES is 1 MB.
Addon stores {id, count} with no name. Name search is two steps, only first touches network:
- Resolve words to ids against
/data/wow/search/itemon ClientCredentials token. Region independent on oauth.battle.net. - Match ids against stored bags, bank, reagent bank, warbank tabs via
findItems(). Pure, inwarband-import.ts.
Resolving query not inventory is why it is affordable. Full warband holds thousands of stacks across hundreds of ids. Resolving inventory would be one call per item. Resolving query is one per search and does not grow with warband.
Numeric query skips step one. That is degradation path. When Game Data unreachable route returns nameSearch: 'unavailable' and UI says "name lookup unavailable, search by item id" rather than empty warband.
Counting rules:
- Warbank counted once, not per character. One vault, same reason it has own table.
- Hit stamped from containers matched in. Bank-only hit takes
seenAt.banknever borrows bag stamp.
Filter composes with other predicates on /characters. held is SortKey excluded from persisted sort keys so restoring sort over absent column cannot show every row reading 0.
Second view on /gear. src/lib/cleanup.ts cleanupAudit() pure, encodeCleanupString() canonical JSON, raw deflate, base64url, wbc1!. No migration, preferences in localStorage.
Site sends one entry per distinct item string, duplicates collapsed. Addon matches on s only, walks live bags at display time, never carries coordinates. wbc1! 40 KB input cap, 512 KB decoded, JSON depth 16.
Rail dispatches wb:import on success and surfaces that read facts re-fetch. Nothing reloads still holds for block, but camp plan left showing pre-import advice is worse than reorder. This is one moment reorder under cursor is not surprise. It is what user pressed enter to cause. Announced not reached for, because rail has no business knowing which surface is mounted.
Any element whose raw text begins with // inside Astro expression is deleted from output silently. Compiler reads // as line comment. No error.
Fix: never write literal // in .cmt text. Mark is .cmt::before CSS. Literal doubles when rendered and deletes when absent. Always defect.
- HTTPS and user gesture required. Warband.pro is HTTPS so Chrome and Edge work.
- Never polling
readText, only inside click. - Do not auto-submit. User confirms after seeing staleness.
- Firefox and Safari stricter -> fallback to text area
Ctrl+V.
After confirm, scorer re-reads consumables rollup (tonight.ts, warband-import.ts types). Freshness changes block to grey text. Vault rows let it name which slot grows on next boss, and warn when API vault is pull behind. Gear owned count and cleanup warnings feed gear view.
Level from addon is only store that can know about ding before logout, authoritative exactly once, never on own. resolveLevel max across three stores.
Maintained alongside Warband.pro App and Addon. Syncs on push. Companion for Warband.pro. MIT. Draft version 0 until CurseForge live.
Start
Integration
Internals