chore: tidy .gitignore and update NOTES.md with build discoveries

This commit is contained in:
Mavis 2026-09-17 23:08:38 +02:00
parent 22854074ca
commit cc4906fba5
7 changed files with 70 additions and 882 deletions

3
.gitignore vendored
View file

@ -8,5 +8,8 @@ build/
data/review/*.json
data/review/*.txt
data/review/unparseable.txt
scripts/__pycache__/
scripts/*.pyc
.next-dev.log
.DS_Store
*.log

View file

@ -91,3 +91,70 @@ Verified by reading both files for invoice 154923 — same data, different PDF l
- "Customer Order No" field on this invoice reads literally `Email 28.07.25` (looks like a separate "Email [date]" reference the supplier uses). The importer will capture it as-is. User can clean later.
- The invoice is stamped **PAID IN FULL**. Some invoices are pre-paid (proforma-paid or settled-on-delivery). We don't track this in the schema yet (future phase) but it's visible on the original PDF.
## 2026-09-17 — Build phase
### Major wins
- ✅ Next.js 14 + Tailwind app on **http://localhost:6002**
- ✅ PB collections: JEI_invoices (15 fields + id), JEI_invoice_lines (8 fields + relations), JEI_stock_codes (4 fields + id) — 30 fields total
- ✅ PDF extractor parses 38 records from 33 JEI supplier PDFs (skips `_non_jei/`)
- ✅ Importer pushed 24 invoices + 70 stock codes + 118 lines into PB (idempotent — re-runs skip duplicates)
- ✅ All 5 UI pages return 200 with real data
### Critical PB v0.39.8 API quirks discovered
1. **POST /api/collections uses key `fields` not `schema`.** Docs show `schema` but the API silently accepts `schema` and stores under `fields`. Always send `fields: [...]`.
2. **Field options go at the FIELD ROOT, not inside `options`.** This is the opposite of what docs for older PB versions show. e.g.
- ✅ WORKS: `{ name: "foo", type: "select", maxSelect: 1, values: ["a","b"] }`
- ❌ FAILS: `{ name: "foo", type: "select", options: { maxSelect: 1, values: ["a","b"] } }` — returns `fields.X.values.code: validation_required, Cannot be blank.` even though the values array is non-empty.
- Same applies to relation fields: `collectionId` and `cascadeDelete` go at the field root, not in `options`.
3. **GET response hides field options.** After creating a collection, `GET /api/collections/<id>` returns `fields` with `options: {}` (empty) for select/relation fields. The data IS stored — just not returned. PATCH the same field with explicit options to confirm (it succeeds).
4. **PATCH round-trip "wakes up" newly created collections.** Same trick as the existing memory entry — if a collection 403s on read or returns empty fields, PATCHing any rule field (even with the existing value) often fixes it.
### Parser quirks remaining
- 4 unparseable PDFs (empty text — likely failed exports: Invoice.pdf, JEI Supplier Invoice.pdf, JEI SA Invoice 147842.pdf, JEI SA Proforma 148931.pdf).
- Some multi-page PDFs (151778, 151779) have page-boundary issues — the parser creates multiple records from the same physical invoice when it shouldn't.
- "JEISA000" got imported as a stock code (it's actually the buyer's account reference) — minor data issue, can be deleted via admin UI.
- 161453, 161852: lines parsed as 0 (one of the page variants has a different layout that the parser doesn't recognize).
### Token expiry
PB token expires **2026-09-18 19:10 SAST** (~22h from now). Re-grab from DevTools → Application → Local Storage → `pocketbase_auth.token` on the PB admin UI when needed.
### File layout (final)
```
JEI/
├── .env.local # PB_URL, PB_TOKEN, NEXT_PUBLIC_PB_URL
├── .gitignore
├── NOTES.md
├── package.json # next 14, pocketbase, tailwind, etc.
├── tsconfig.json
├── next.config.js
├── tailwind.config.ts
├── postcss.config.js
├── app/
│ ├── layout.tsx # header with nav
│ ├── page.tsx # dashboard
│ ├── invoices/page.tsx # list with search
│ ├── invoices/[id]/page.tsx # detail with PDF link
│ ├── stock-codes/page.tsx # list with stats
│ ├── stock-codes/[code]/page.tsx # detail with avg/min/max
│ ├── globals.css
│ └── lib/{pb.ts, types.ts}
├── scripts/
│ ├── create_collections.py # creates 3 JEI_* collections
│ ├── extract_invoices.py # PDF → data/review/all.json
│ ├── import_to_pb.py # all.json → PB (idempotent)
│ └── patch_collections.py # legacy workaround (not needed anymore)
├── invoices/ # user drops PDFs here
├── data/review/all.json # parsed records
└── docs/superpowers/specs/... # design spec
```
### How to use
1. Server runs on `http://localhost:6002` via `pnpm dev` (or `node_modules/.bin/next.cmd dev -p 6002`).
2. To re-extract: `python scripts/extract_invoices.py` (reads all PDFs in `invoices/`, writes `data/review/all.json`).
3. To re-import: `python scripts/import_to_pb.py` (skips existing invoice_no + document_type pairs).
4. To refresh PB token: visit `https://pb.agent.apphub.co.za/_/`, copy `pocketbase_auth.token` from DevTools → Application → Local Storage, paste into `JEI/.env.local`.

View file

@ -1,122 +0,0 @@
"""
PATCH schema fields onto the JEI_* collections if they're missing.
Workaround for a known PocketBase quirk where collections created via API
sometimes persist with empty schema. The rules PATCH trick works for empty
rules; for empty schema we explicitly PATCH the schema field.
Run: python scripts/patch_collections.py
"""
import os
import sys
import json
import urllib.request
from urllib.error import HTTPError
PB_URL = os.environ.get("PB_URL", "https://pb.agent.apphub.co.za")
PB_TOKEN = os.environ.get("PB_TOKEN", "")
if not PB_TOKEN:
env_path = os.path.join(os.path.dirname(__file__), "..", ".env.local")
if os.path.exists(env_path):
for line in open(env_path):
if line.startswith("PB_TOKEN="):
PB_TOKEN = line.strip().split("=", 1)[1]
if not PB_TOKEN:
sys.exit("ERROR: PB_TOKEN not set")
HEADERS = {"Authorization": PB_TOKEN, "Content-Type": "application/json"}
def pb_get(path):
req = urllib.request.Request(PB_URL + path, headers=HEADERS, method="GET")
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def pb_patch(path, body):
req = urllib.request.Request(PB_URL + path, data=json.dumps(body).encode("utf-8"),
headers=HEADERS, method="PATCH")
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read()), None
except HTTPError as e:
return None, e.read().decode()
# Mirror of create_collections.py schemas (kept independent for clarity)
SUPERUSER_RULES = {"listRule": "", "viewRule": "", "createRule": "",
"updateRule": "", "deleteRule": ""}
def invoices_schema():
return [
{"name": "invoice_no", "type": "text", "required": True, "options": {"min": 1, "max": 50, "pattern": ""}},
{"name": "invoice_date", "type": "date", "required": True, "options": {"min": "", "max": ""}},
{"name": "document_type", "type": "select", "required": True, "options": {"maxSelect": 1, "values": ["invoice", "proforma", "quote"]}},
{"name": "customer_order_no", "type": "text", "required": False, "options": {"min": 0, "max": 100, "pattern": ""}},
{"name": "buyer_name_variant", "type": "select", "required": True, "options": {"maxSelect": 1, "values": ["t/a JEI South Africa", "JEI South Africa", "JEI South Africa Importers"]}},
{"name": "supplier_name", "type": "text", "required": True, "options": {"min": 0, "max": 200, "pattern": ""}},
{"name": "account_ref", "type": "text", "required": False, "options": {"min": 0, "max": 50, "pattern": ""}},
{"name": "total_net", "type": "number", "required": True, "options": {"min": 0, "max": None, "noDecimal": False}},
{"name": "carriage_net", "type": "number", "required": False, "options": {"min": 0, "max": None, "noDecimal": False}},
{"name": "invoice_total", "type": "number", "required": True, "options": {"min": 0, "max": None, "noDecimal": False}},
{"name": "deliver_to_name", "type": "text", "required": False, "options": {"min": 0, "max": 200, "pattern": ""}},
{"name": "deliver_to_address", "type": "text", "required": False, "options": {"min": 0, "max": 1000, "pattern": ""}},
{"name": "source_filename", "type": "text", "required": False, "options": {"min": 0, "max": 500, "pattern": ""}},
{"name": "notes", "type": "text", "required": False, "options": {"min": 0, "max": 5000, "pattern": ""}},
{"name": "pdf_file", "type": "file", "required": False, "options": {"maxSelect": 1, "maxSize": 0, "mimeTypes": ["application/pdf"], "thumbs": []}},
]
def stock_codes_schema():
return [
{"name": "code", "type": "text", "required": True, "options": {"min": 1, "max": 100, "pattern": ""}},
{"name": "description_goods", "type": "text", "required": True, "options": {"min": 1, "max": 500, "pattern": ""}},
{"name": "first_seen_date", "type": "date", "required": False, "options": {"min": "", "max": ""}},
{"name": "last_seen_date", "type": "date", "required": False, "options": {"min": "", "max": ""}},
]
def lines_schema():
# We need to resolve the relation target ids
cols = pb_get("/api/collections?perPage=200")["items"]
inv = next(c for c in cols if c["name"] == "JEI_invoices")
sc = next(c for c in cols if c["name"] == "JEI_stock_codes")
return [
{"name": "invoice", "type": "relation", "required": True, "options": {"collectionId": inv["id"], "cascadeDelete": True, "minSelect": 1, "maxSelect": 1}},
{"name": "line_no", "type": "number", "required": True, "options": {"min": 0, "max": None, "noDecimal": True}},
{"name": "stock_code", "type": "relation", "required": True, "options": {"collectionId": sc["id"], "cascadeDelete": True, "minSelect": 1, "maxSelect": 1}},
{"name": "quantity", "type": "number", "required": True, "options": {"min": 0, "max": None, "noDecimal": False}},
{"name": "unit_price", "type": "number", "required": True, "options": {"min": 0, "max": None, "noDecimal": False}},
{"name": "net_amount", "type": "number", "required": True, "options": {"min": 0, "max": None, "noDecimal": False}},
{"name": "commodity_code", "type": "text", "required": False, "options": {"min": 0, "max": 50, "pattern": ""}},
{"name": "country_of_origin", "type": "text", "required": False, "options": {"min": 0, "max": 50, "pattern": ""}},
]
PATCHES = [
("JEI_invoices", invoices_schema),
("JEI_stock_codes", stock_codes_schema),
("JEI_invoice_lines", lines_schema),
]
def main():
cols = pb_get("/api/collections?perPage=200")["items"]
for name, factory in PATCHES:
col = next((c for c in cols if c["name"] == name), None)
if not col:
print(f"SKIP: {name} does not exist")
continue
if col.get("schema") and len(col["schema"]) > 0:
print(f"OK: {name} already has {len(col['schema'])} fields")
continue
body = {"schema": factory(), **SUPERUSER_RULES}
result, err = pb_patch(f"/api/collections/{col['id']}", body)
if err:
print(f"FAIL: {name} — {err}")
sys.exit(1)
print(f"PATCHED: {name} → {len(result.get('schema', []))} fields")
if __name__ == "__main__":
main()

View file

@ -1,674 +0,0 @@
uid=1_0 RootWebArea "ysterbaard_annual_returns - Acme" url="https://pb.agent.apphub.co.za/_/#/collections?collection=ysterbaard_annual_returns"
uid=1_1 main
uid=2_0 link "App logo" url="https://pb.agent.apphub.co.za/_/#/"
uid=2_1 image "App logo" url="https://pb.agent.apphub.co.za/_/images/logo_white.svg"
uid=2_2 navigation
uid=2_3 link "Collections" url="https://pb.agent.apphub.co.za/_/#/collections"
uid=2_4 StaticText "Collections"
uid=2_5 link "Logs" url="https://pb.agent.apphub.co.za/_/#/logs"
uid=2_6 StaticText "Logs"
uid=2_7 link "Settings" url="https://pb.agent.apphub.co.za/_/#/settings"
uid=2_8 StaticText "Settings"
uid=2_9 button "Color scheme" expandable
uid=2_10 button "ysterbaard@apphub.co.za" expandable
uid=2_11 complementary
uid=2_12 textbox "Search collections..."
uid=2_13 button description="Collections overview"
uid=2_14 navigation
uid=2_15 DisclosureTriangle "Collections" expandable expanded
uid=2_16 button "users "
uid=2_17 button "" description="Pin"
uid=2_18 button "groups "
uid=2_19 button "" description="Pin"
uid=2_20 button "sable_api "
uid=2_21 button "" description="Pin"
uid=2_22 button "sable_notes "
uid=2_23 button "" description="Pin"
uid=2_24 button "tasks "
uid=2_25 button "" description="Pin"
uid=2_26 button "test_patch_schema "
uid=2_27 button "" description="Pin"
uid=2_28 button "ysterbaard_SARS_EMP201_PAYE "
uid=2_29 button "" description="Pin"
uid=2_30 button "ysterbaard_SARS_VAT_VAT201 "
uid=2_31 button "" description="Pin"
uid=2_32 button "ysterbaard_annual_returns "
uid=2_33 button "" description="Pin"
uid=2_34 button "ysterbaard_companies "
uid=2_35 button "" description="Pin"
uid=2_36 button "ysterbaard_company_directors "
uid=2_37 button "" description="Pin"
uid=2_38 button "ysterbaard_company_documents "
uid=2_39 button "" description="Pin"
uid=2_40 button "ysterbaard_company_emails "
uid=2_41 button "" description="Pin"
uid=2_42 button "ysterbaard_company_registrations "
uid=2_43 button "" description="Pin"
uid=2_44 button "ysterbaard_director_appointments "
uid=2_45 button "" description="Pin"
uid=2_46 button "ysterbaard_director_documents "
uid=2_47 button "" description="Pin"
uid=2_48 button "ysterbaard_director_resignations "
uid=2_49 button "" description="Pin"
uid=2_50 button "ysterbaard_directors "
uid=2_51 button "" description="Pin"
uid=2_52 button "ysterbaard_document_types "
uid=2_53 button "" description="Pin"
uid=2_54 button "ysterbaard_notes "
uid=2_55 button "" description="Pin"
uid=2_56 button "ysterbaard_onboarding_submissions "
uid=2_57 button "" description="Pin"
uid=2_58 button "ysterbaard_reg_directors "
uid=2_59 button "" description="Pin"
uid=2_60 button "ysterbaard_reg_shareholders "
uid=2_61 button "" description="Pin"
uid=2_62 button "ysterbaard_registration_emails "
uid=2_63 button "" description="Pin"
uid=2_64 button "ysterbaard_resolution_attendees "
uid=2_65 button "" description="Pin"
uid=2_66 button "ysterbaard_resolution_received_letters "
uid=2_67 button "" description="Pin"
uid=2_68 button "ysterbaard_resolution_signers "
uid=2_69 button "" description="Pin"
uid=2_70 button "ysterbaard_resolutions "
uid=2_71 button "" description="Pin"
uid=2_72 button "ysterbaard_sars_irp6_income_tax "
uid=2_73 button "" description="Pin"
uid=2_74 DisclosureTriangle " System" expandable
uid=2_75 button "New collection"
uid=2_76 navigation
uid=2_77 StaticText "Collections"
uid=1_5 StaticText "/"
uid=2_78 StaticText "ysterbaard_annual_returns"
uid=2_79 button "Collection settings"
uid=2_80 button "Refresh"
uid=2_81 button "API preview"
uid=2_82 button "New record"
uid=2_83 form
uid=2_84 button "Search history" expandable
uid=2_85 textbox multiline
uid=2_86 checkbox ""
uid=1_5 StaticText ""
uid=2_87 StaticText "id"
uid=2_88 StaticText "company"
uid=2_89 StaticText "ar_year"
uid=2_90 StaticText "ar_month"
uid=2_91 StaticText "ar_non_compliance_date"
uid=2_92 StaticText "is_filed"
uid=2_93 StaticText "amount_paid"
uid=2_94 StaticText "date_filed"
uid=2_95 StaticText "source"
uid=2_96 button "Toggle columns" expandable
uid=2_97 row
uid=2_98 checkbox ""
uid=1_5 StaticText ""
uid=2_99 button "Copy"
uid=2_100 StaticText "vzusowawsnvryc4c4q5ib"
uid=2_101 StaticText "True"
uid=2_102 StaticText "2026"
uid=2_103 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_104 StaticText "False"
uid=2_105 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_106 StaticText "manual"
uid=2_107 row
uid=2_108 checkbox ""
uid=1_5 StaticText ""
uid=2_109 button "Copy"
uid=2_110 StaticText "utbehhh45ivqxsr3stzg4"
uid=2_111 StaticText "True"
uid=2_112 StaticText "2025"
uid=2_113 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_114 StaticText "False"
uid=2_115 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_116 StaticText "manual"
uid=2_117 row
uid=2_118 checkbox ""
uid=1_5 StaticText ""
uid=2_119 button "Copy"
uid=2_120 StaticText "lxsgxus2d2earitha3o8r"
uid=2_121 StaticText "True"
uid=2_122 StaticText "2024"
uid=2_123 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_124 StaticText "False"
uid=2_125 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_126 StaticText "manual"
uid=2_127 row
uid=2_128 checkbox ""
uid=1_5 StaticText ""
uid=2_129 button "Copy"
uid=2_130 StaticText "vp7e1adhqtc30qc8ucojn"
uid=2_131 StaticText "True"
uid=2_132 StaticText "2023"
uid=2_133 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_134 StaticText "False"
uid=2_135 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_136 StaticText "manual"
uid=2_137 row
uid=2_138 checkbox ""
uid=1_5 StaticText ""
uid=2_139 button "Copy"
uid=2_140 StaticText "qrplpjb1s3jcsmv05lfn6"
uid=2_141 StaticText "True"
uid=2_142 StaticText "2022"
uid=2_143 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_144 StaticText "False"
uid=2_145 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_146 StaticText "manual"
uid=2_147 row
uid=2_148 checkbox ""
uid=1_5 StaticText ""
uid=2_149 button "Copy"
uid=2_150 StaticText "mvjd01oe6rh6khh4fbmvo"
uid=2_151 StaticText "True"
uid=2_152 StaticText "2021"
uid=2_153 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_154 StaticText "False"
uid=2_155 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_156 StaticText "manual"
uid=2_157 row
uid=2_158 checkbox ""
uid=1_5 StaticText ""
uid=2_159 button "Copy"
uid=2_160 StaticText "3f1a3miti26r4elhcvkkx"
uid=2_161 StaticText "True"
uid=2_162 StaticText "2020"
uid=2_163 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_164 StaticText "False"
uid=2_165 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_166 StaticText "manual"
uid=2_167 row
uid=2_168 checkbox ""
uid=1_5 StaticText ""
uid=2_169 button "Copy"
uid=2_170 StaticText "x93s1dx044zjm6v5dgaty"
uid=2_171 StaticText "True"
uid=2_172 StaticText "2026"
uid=2_173 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_174 StaticText "False"
uid=2_175 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_176 StaticText "manual"
uid=2_177 row
uid=2_178 checkbox ""
uid=1_5 StaticText ""
uid=2_179 button "Copy"
uid=2_180 StaticText "r1elimr6oe4jbmi7mj4o4"
uid=2_181 StaticText "True"
uid=2_182 StaticText "2025"
uid=2_183 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_184 StaticText "False"
uid=2_185 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_186 StaticText "manual"
uid=2_187 row
uid=2_188 checkbox ""
uid=1_5 StaticText ""
uid=2_189 button "Copy"
uid=2_190 StaticText "1m56e3az8hbbtympbgyyv"
uid=2_191 StaticText "True"
uid=2_192 StaticText "2024"
uid=2_193 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_194 StaticText "False"
uid=2_195 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_196 StaticText "manual"
uid=2_197 row
uid=2_198 checkbox ""
uid=1_5 StaticText ""
uid=2_199 button "Copy"
uid=2_200 StaticText "eyjrz9r5o9vvyad651e93"
uid=2_201 StaticText "True"
uid=2_202 StaticText "2023"
uid=2_203 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_204 StaticText "False"
uid=2_205 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_206 StaticText "manual"
uid=2_207 row
uid=2_208 checkbox ""
uid=1_5 StaticText ""
uid=2_209 button "Copy"
uid=2_210 StaticText "oqs26zkb963yrxwci7uty"
uid=2_211 StaticText "True"
uid=2_212 StaticText "2022"
uid=2_213 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_214 StaticText "False"
uid=2_215 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_216 StaticText "manual"
uid=2_217 row
uid=2_218 checkbox ""
uid=1_5 StaticText ""
uid=2_219 button "Copy"
uid=2_220 StaticText "g57qpwq4mk11y0iox96gd"
uid=2_221 StaticText "True"
uid=2_222 StaticText "2021"
uid=2_223 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_224 StaticText "False"
uid=2_225 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_226 StaticText "manual"
uid=2_227 row
uid=2_228 checkbox ""
uid=1_5 StaticText ""
uid=2_229 button "Copy"
uid=2_230 StaticText "newgkeoeixf2tn3ddsm4o"
uid=2_231 StaticText "True"
uid=2_232 StaticText "2020"
uid=2_233 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_234 StaticText "False"
uid=2_235 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_236 StaticText "manual"
uid=2_237 row
uid=2_238 checkbox ""
uid=1_5 StaticText ""
uid=2_239 button "Copy"
uid=2_240 StaticText "px32hbbepgd8g4k3x6jw2"
uid=2_241 StaticText "True"
uid=2_242 StaticText "2026"
uid=2_243 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_244 StaticText "False"
uid=2_245 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_246 StaticText "manual"
uid=2_247 row
uid=2_248 checkbox ""
uid=1_5 StaticText ""
uid=2_249 button "Copy"
uid=2_250 StaticText "e9dfpce06qbnszabfagnb"
uid=2_251 StaticText "True"
uid=2_252 StaticText "2025"
uid=2_253 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_254 StaticText "False"
uid=2_255 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_256 StaticText "manual"
uid=2_257 row
uid=2_258 checkbox ""
uid=1_5 StaticText ""
uid=2_259 button "Copy"
uid=2_260 StaticText "rt0i5vcyefxz9a8b82q0s"
uid=2_261 StaticText "True"
uid=2_262 StaticText "2024"
uid=2_263 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_264 StaticText "False"
uid=2_265 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_266 StaticText "manual"
uid=2_267 row
uid=2_268 checkbox ""
uid=1_5 StaticText ""
uid=2_269 button "Copy"
uid=2_270 StaticText "zj57br33bmi80fkktefoi"
uid=2_271 StaticText "True"
uid=2_272 StaticText "2023"
uid=2_273 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_274 StaticText "False"
uid=2_275 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_276 StaticText "manual"
uid=2_277 row
uid=2_278 checkbox ""
uid=1_5 StaticText ""
uid=2_279 button "Copy"
uid=2_280 StaticText "ckrqqcb2osh1n6ad053y5"
uid=2_281 StaticText "True"
uid=2_282 StaticText "2022"
uid=2_283 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_284 StaticText "False"
uid=2_285 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_286 StaticText "manual"
uid=2_287 row
uid=2_288 checkbox ""
uid=1_5 StaticText ""
uid=2_289 button "Copy"
uid=2_290 StaticText "05y3njez84fa17mt263pu"
uid=2_291 StaticText "True"
uid=2_292 StaticText "2021"
uid=2_293 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_294 StaticText "False"
uid=2_295 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_296 StaticText "manual"
uid=2_297 row
uid=2_298 checkbox ""
uid=1_5 StaticText ""
uid=2_299 button "Copy"
uid=2_300 StaticText "aw4zn92g86sr0v24xtcmu"
uid=2_301 StaticText "True"
uid=2_302 StaticText "2020"
uid=2_303 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_304 StaticText "False"
uid=2_305 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_306 StaticText "manual"
uid=2_307 row
uid=2_308 checkbox ""
uid=1_5 StaticText ""
uid=2_309 button "Copy"
uid=2_310 StaticText "hh4r8l6vgvcvq3d42xe4y"
uid=2_311 StaticText "True"
uid=2_312 StaticText "2026"
uid=2_313 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_314 StaticText "False"
uid=2_315 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_316 StaticText "bizportal"
uid=2_317 row
uid=2_318 checkbox ""
uid=1_5 StaticText ""
uid=2_319 button "Copy"
uid=2_320 StaticText "aro8cui2k9x7y2nhuiou3"
uid=2_321 StaticText "True"
uid=2_322 StaticText "2025"
uid=2_323 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_324 StaticText "False"
uid=2_325 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_326 StaticText "bizportal"
uid=2_327 row
uid=2_328 checkbox ""
uid=1_5 StaticText ""
uid=2_329 button "Copy"
uid=2_330 StaticText "nmu5nsrdytqsl1p2j51d3"
uid=2_331 StaticText "True"
uid=2_332 StaticText "2024"
uid=2_333 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_334 StaticText "False"
uid=2_335 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_336 StaticText "bizportal"
uid=2_337 row
uid=2_338 checkbox ""
uid=1_5 StaticText ""
uid=2_339 button "Copy"
uid=2_340 StaticText "7m4qh6vfdn6yknzyia90f"
uid=2_341 StaticText "True"
uid=2_342 StaticText "2023"
uid=2_343 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_344 StaticText "False"
uid=2_345 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_346 StaticText "bizportal"
uid=2_347 row
uid=2_348 checkbox ""
uid=1_5 StaticText ""
uid=2_349 button "Copy"
uid=2_350 StaticText "gdk19i23qb89onuwvcgso"
uid=2_351 StaticText "True"
uid=2_352 StaticText "2022"
uid=2_353 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_354 StaticText "False"
uid=2_355 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_356 StaticText "bizportal"
uid=2_357 row
uid=2_358 checkbox ""
uid=1_5 StaticText ""
uid=2_359 button "Copy"
uid=2_360 StaticText "kod1zxa0dx1jc870avlkz"
uid=2_361 StaticText "True"
uid=2_362 StaticText "2021"
uid=2_363 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_364 StaticText "False"
uid=2_365 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_366 StaticText "bizportal"
uid=2_367 row
uid=2_368 checkbox ""
uid=1_5 StaticText ""
uid=2_369 button "Copy"
uid=2_370 StaticText "e9bmkdv2z0jel4w5cv9qa"
uid=2_371 StaticText "True"
uid=2_372 StaticText "2020"
uid=2_373 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_374 StaticText "False"
uid=2_375 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_376 StaticText "bizportal"
uid=2_377 row
uid=2_378 checkbox ""
uid=1_5 StaticText ""
uid=2_379 button "Copy"
uid=2_380 StaticText "2v2nos72ysckf6q6awkn5"
uid=2_381 StaticText "True"
uid=2_382 StaticText "2026"
uid=2_383 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_384 StaticText "False"
uid=2_385 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_386 StaticText "manual"
uid=2_387 row
uid=2_388 checkbox ""
uid=1_5 StaticText ""
uid=2_389 button "Copy"
uid=2_390 StaticText "usodm5otthx7d2wro8bwx"
uid=2_391 StaticText "True"
uid=2_392 StaticText "2025"
uid=2_393 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_394 StaticText "False"
uid=2_395 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_396 StaticText "manual"
uid=2_397 row
uid=2_398 checkbox ""
uid=1_5 StaticText ""
uid=2_399 button "Copy"
uid=2_400 StaticText "euf62r18bbgdn37aw4jrl"
uid=2_401 StaticText "True"
uid=2_402 StaticText "2024"
uid=2_403 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_404 StaticText "False"
uid=2_405 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_406 StaticText "manual"
uid=2_407 row
uid=2_408 checkbox ""
uid=1_5 StaticText ""
uid=2_409 button "Copy"
uid=2_410 StaticText "ii73b9ovz2pq5ug9pdt5b"
uid=2_411 StaticText "True"
uid=2_412 StaticText "2023"
uid=2_413 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_414 StaticText "False"
uid=2_415 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_416 StaticText "manual"
uid=2_417 row
uid=2_418 checkbox ""
uid=1_5 StaticText ""
uid=2_419 button "Copy"
uid=2_420 StaticText "auf2h2a96of5xw5hnmf2n"
uid=2_421 StaticText "True"
uid=2_422 StaticText "2022"
uid=2_423 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_424 StaticText "False"
uid=2_425 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_426 StaticText "manual"
uid=2_427 row
uid=2_428 checkbox ""
uid=1_5 StaticText ""
uid=2_429 button "Copy"
uid=2_430 StaticText "amcbd5kbuc3bmw4m2qdiv"
uid=2_431 StaticText "True"
uid=2_432 StaticText "2021"
uid=2_433 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_434 StaticText "False"
uid=2_435 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_436 StaticText "manual"
uid=2_437 row
uid=2_438 checkbox ""
uid=1_5 StaticText ""
uid=2_439 button "Copy"
uid=2_440 StaticText "v0qye9ln762yfdlq7d4ch"
uid=2_441 StaticText "True"
uid=2_442 StaticText "2020"
uid=2_443 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_444 StaticText "False"
uid=2_445 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_446 StaticText "manual"
uid=2_447 row
uid=2_448 checkbox ""
uid=1_5 StaticText ""
uid=2_449 button "Copy"
uid=2_450 StaticText "k3pbizpi0iaymplwy0fa1"
uid=2_451 StaticText "True"
uid=2_452 StaticText "2026"
uid=2_453 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_454 StaticText "False"
uid=2_455 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_456 StaticText "manual"
uid=2_457 row
uid=2_458 checkbox ""
uid=1_5 StaticText ""
uid=2_459 button "Copy"
uid=2_460 StaticText "bqd0glsquey9b7z4w2k50"
uid=2_461 StaticText "True"
uid=2_462 StaticText "2025"
uid=2_463 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_464 StaticText "False"
uid=2_465 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_466 StaticText "manual"
uid=2_467 row
uid=2_468 checkbox ""
uid=1_5 StaticText ""
uid=2_469 button "Copy"
uid=2_470 StaticText "xx9zhklos3e90w7opej5s"
uid=2_471 StaticText "True"
uid=2_472 StaticText "2024"
uid=2_473 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_474 StaticText "False"
uid=2_475 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_476 StaticText "manual"
uid=2_477 row
uid=2_478 checkbox ""
uid=1_5 StaticText ""
uid=2_479 button "Copy"
uid=2_480 StaticText "p4wn8dtp9qi1jh87gtt9g"
uid=2_481 StaticText "True"
uid=2_482 StaticText "2023"
uid=2_483 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_484 StaticText "False"
uid=2_485 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_486 StaticText "manual"
uid=2_487 row
uid=2_488 checkbox ""
uid=1_5 StaticText ""
uid=2_489 button "Copy"
uid=2_490 StaticText "3dj84c5u39hrv39ak8f7x"
uid=2_491 StaticText "True"
uid=2_492 StaticText "2022"
uid=2_493 StaticText "JANUARY"
uid=1_5 StaticText "N/A"
uid=2_494 StaticText "False"
uid=2_495 StaticText "0"
uid=1_5 StaticText "N/A"
uid=2_496 StaticText "manual"
uid=2_497 button "Load more"
uid=2_498 StaticText "Total: "
uid=2_499 StaticText "449"
uid=2_500 link "Docs" url="https://pocketbase.io/docs"
uid=2_501 StaticText "Docs"
uid=2_502 link "PocketBase v0.39.8" url="https://github.com/pocketbase/pocketbase/releases"
uid=2_503 StaticText "PocketBase v0.39.8"
uid=3_0 generic
uid=3_1 banner
uid=3_2 heading "Create collection" level="6"
uid=3_3 StaticText "Name"
uid=1_5 StaticText "*"
uid=3_4 textbox "Name *" required value="JEI_stock_codes"
uid=3_5 button "Type: Base" expandable
uid=3_6 navigation
uid=3_7 button "Fields"
uid=3_8 button "API rules"
uid=5_0 DisclosureTriangleGrouped "Field options" expandable
uid=1_5 StaticText ""
uid=5_1 banner
uid=1_5 StaticText ""
uid=5_2 textbox "" disableable disabled required value="id"
uid=5_3 StaticText "Required"
uid=5_4 button "Field options"
uid=5_5 DisclosureTriangleGrouped "Field options" expandable
uid=1_5 StaticText ""
uid=5_6 banner
uid=1_5 StaticText ""
uid=5_7 textbox "" focusable focused required value="text"
uid=5_8 button "Field options"
uid=5_9 DisclosureTriangleGrouped "Field options" expandable
uid=1_5 StaticText ""
uid=5_10 banner
uid=1_5 StaticText ""
uid=5_11 textbox "" required value="created"
uid=5_12 button "Create " expandable
uid=5_13 button "Field options"
uid=5_14 DisclosureTriangleGrouped "Field options" expandable
uid=1_5 StaticText ""
uid=5_15 banner
uid=1_5 StaticText ""
uid=5_16 textbox "" required value="updated"
uid=5_17 button "Create/Update " expandable
uid=5_18 button "Field options"
uid=3_24 button "New field" expandable
uid=3_25 StaticText "Unique constraints and indexes ("
uid=4_13 StaticText "0"
uid=3_27 StaticText ")"
uid=3_28 button "New index"
uid=3_29 contentinfo
uid=3_30 button "Close"
uid=3_31 button "Create"
uid=3_32 button "Save options" expandable

View file

@ -1,26 +0,0 @@
const { default: PocketBase } = require('pocketbase');
const fs = require('fs');
const envFile = fs.readFileSync('.env.local', 'utf8');
const token = envFile.match(/PB_TOKEN=(.+)/)[1].trim();
const url = envFile.match(/PB_URL=(.+)/)[1].trim();
const pb = new PocketBase(url);
pb.authStore.save(token);
(async () => {
try {
const col = await pb.collections.create({
name: 'JEI_test_sdk',
type: 'base',
fields: [
{ name: 'foo', type: 'select', required: false, options: { maxSelect: 1, values: ['a','b'] } }
],
listRule: '', viewRule: '', createRule: '', updateRule: '', deleteRule: ''
});
console.log('Created id=' + col.id + ' fields=' + col.fields.length);
await pb.collections.delete(col.id);
console.log('deleted');
} catch (e) {
console.log('ERR: ' + e.message);
if (e.data) console.log('data: ' + JSON.stringify(e.data));
}
})();

View file

@ -1,60 +0,0 @@
// Test creating a select field with various formats to find what works
const { default: PocketBase } = require('pocketbase');
const fs = require('fs');
const envFile = fs.readFileSync('.env.local', 'utf8');
const token = envFile.match(/PB_TOKEN=(.+)/)[1].trim();
const url = envFile.match(/PB_URL=(.+)/)[1].trim();
const pb = new PocketBase(url);
pb.authStore.save(token);
async function test(name, fields) {
try {
const col = await pb.collections.create({
name: 'JEI_t_' + Date.now() + '_' + name,
type: 'base',
fields,
listRule: '', viewRule: '', createRule: '', updateRule: '', deleteRule: ''
});
console.log(`OK [${name}]: id=${col.id} fields=${col.fields.length}`);
// Get it back
const got = await pb.collections.getOne(col.id);
console.log(` fields detail:`);
got.fields.forEach(f => console.log(` - ${f.name} (${f.type}) opts=${JSON.stringify(f.options || {})}`));
// Cleanup
await pb.collections.delete(col.id);
return true;
} catch (e) {
console.log(`FAIL [${name}]: ${e.message}`);
if (e.data) console.log(' data: ' + JSON.stringify(e.data));
return false;
}
}
(async () => {
// Test 1: select with explicit id + system:false + values only
await test('sel_basic', [
{ id: 'select_aaa', name: 'foo', type: 'select', system: false, required: true, options: { maxSelect: 1, values: ['a','b'] } }
]);
// Test 2: select with maxSelect as string
await test('sel_str', [
{ name: 'foo', type: 'select', options: { values: ['a','b'] } }
]);
// Test 3: multi-select
await test('sel_multi', [
{ name: 'foo', type: 'select', options: { maxSelect: 2, values: ['a','b','c'] } }
]);
// Test 4: text first then select
await test('text_then_sel', [
{ name: 'bar', type: 'text' },
{ name: 'foo', type: 'select', options: { maxSelect: 1, values: ['a','b'] } }
]);
// Test 5: select with no maxSelect at all
await test('sel_no_max', [
{ name: 'foo', type: 'select', required: false, options: { values: ['a','b'] } }
]);
})();