Zonqor Refurbishment — project index
Everything in this folder, in one place. Built 17 August 2026
What this is
Kurt's refurbishment of the Zonqor apartment. Roderick (main contractor, with his son) does gypsum, electrical, plumbing and tiling; ULF does carpentry; Christine walks the site each evening, signs items off and agrees prices on paper. Print is the primary medium — everything here is built to be printed.
The two deliverables
- Site Checklist — 23 A4 pages: work plan, day-by-day, open questions, 13 room sections with per-item price and sign-off fields, price summary, appendix of 49 photos.
- Base Plan — reconstructed floor plan, 12 rooms with areas and confidence grades, plus the measure-on-site list. A reconstruction, not a survey.
Open questions — all still unanswered
| OQ-01 | Outdoor wall finish — PVC sheet or tiles? |
| OQ-02 | Outdoor kitchen — design / equipment spec |
| OQ-03 | New consumer unit — location and capacity |
| OQ-04 | Study/Office plugs — quantity |
| OQ-05 | Ensuite tiles — confirm 60×30 dimensions |
| OQ-06 | Living/Dining TV wall — PVC sheet spec (material, size, fixing) |
| OQ-07 | Sofa-area TV unit — electrical point spec |
| OQ-08 | Intercom — reuse or replace? audio or video? |
| OQ-09 | Who books and pays for the skips? |
| OQ-10 | Tiling scope — site photos show the ensuite, main bathroom and front balcony are already tiled. Does ~145 sqm still stand, and what is in it? |
| OQ-11 | Fireplace — the photo shows it installed with the wall already opened beneath. Remove, or keep and rework around it? |
| OQ-12 | Two tile formats on site: Fanal 120 and Fanal 31.6×90. What is the 31.6×90 for? |
The three things to settle first
- OQ-10 tiling scope — before any price is agreed. The base plan shows the 125/145 m² gap is the two balconies; what is still open is how much is actually being tiled (~22 m² of argument).
- OQ-11 fireplace — remove or keep? Needed by plan day 4, Thu 20 Aug.
- OQ-03 consumer unit and OQ-08 intercom — both change first-fix cable, week one.
Site Checklist (PDF)
23 pages. The live document — this is what Christine prints and works from. · open the real PDF (print from that, not from these previews). Click a page to enlarge.























Base Plan (PDF)
3 pages. Reconstructed floor plan, room schedule, and the measure-on-site list. · open the real PDF (print from that, not from these previews). Click a page to enlarge.



Photo Sort Sheet (PDF)
4 pages. Contact sheet used to try to place the 54 unassigned site photos. · open the real PDF (print from that, not from these previews). Click a page to enlarge.




Site Checklist (HTML)
Same document, with live-totalling price boxes. Open full screen to use it.
CLAUDE.md — master context
Zonqor Apartment Refurbishment — Project Context
Read this file first — or open index.html in the project root, which browses this file and everything else in the folder offline. It is the complete context for the project, carried over from a long Cowork session. Everything the previous session knew is either in this file or in the files it points at. Where something is uncertain, it says so — do not treat inference as fact.
1. What this project is
Kurt is refurbishing his apartment at Zonqor (Malta). A contractor starts on Monday 17 August 2026 (delayed from Thursday 13 August). The work is turnkey: one main contractor plus a carpenter.
The artefact built so far is a printed A4 site checklist and price sheet — 81 work items across 13 areas, each with an ID, trade, assignee, dependency, phase, and space to record status, date, agreed price, initials and comments on paper.
Who does what
| Person | Role |
|---|---|
| Roderick | Main contractor. Gypsum & structural, electrical, plumbing & gas, tiling. Works with his son — a crew of 2. |
| ULF | Carpenter. Wardrobes, cabinets, TV unit, mirrors, fittings. (ULF is a person's name, not an acronym.) |
| Christine | Kurt's wife. Does the daily on-site walk-through, signs off items and agrees prices per item, in the evening, on paper. |
| Kurt | Owner. Answers open questions, confirms specs, makes ordering decisions. |
The paper-first design exists because Christine works from a printed copy on site at night. Any future deliverable must keep that property: print is the primary medium, screen is secondary.
2. Why this handoff exists — what to build next
The checklist is essentially done. What is missing and is the reason for moving to Claude Code:
A full architecture / floor plan.DONE, 16 Aug 2026 —output/Zonqor Base Plan.pdf.
Reconstructed by tracing the designer's 1:100 layout, calibrated on a printed scale bar (two independent routes agreeing to 0.08%). 12 rooms with floor areas, per-room confidence grades, and a measure-on-site list. Method and caveats: docs/09-base-plan.md. Geometry lives in build/plan_data.py and nowhere else. It also answered half of OQ-10 — the 125 / 145 sqm gap is the two balconies. ⚠ Still a reconstruction, not a survey: the measure-on-site list is outstanding, and no point positions should be committed until it is done.
- A proper electrical plan. The designer supplied renders but no electrical drawings. The
checklist currently carries only an indicative point count per room (how many light points, switches, power points) — explicitly labelled "not a wiring diagram". What is needed: a circuit schedule, a consumer-unit / distribution board schedule, point layout per room, cable routes, and the ethernet runs back to the entrance hub.
- Elevations. The designer gave "limited elevations" (one PDF). Room elevations showing switch
and socket heights, wardrobe internals, and the feature-wall build-up do not exist.
- Anything else missing — see
docs/08-whats-missing.md.
Honest constraints on doing this
- There is no measured survey. Dimensions exist only where a drawing happens to be annotated.
Any plan produced from this material is approximate and must be labelled as such until someone measures the flat. Do not present a reconstructed plan as a survey drawing.
- No coordinates exist for electrical points. The point counts come from a spreadsheet listing
("2 bedside plugs", "4 ceiling lights"), not from positions. Placement on any drawing is a proposal to be confirmed on site with Roderick, not a record of fact.
- The previous session deliberately refused to fake a wiring diagram for this reason. Keep that
discipline — a confidently-wrong electrical drawing on a live site is dangerous.
3. Current state of the deliverable
output/Zonqor Site Checklist.pdf — 23 A4 pages, the live document. Structure:
- Cover — roles, how-to-use, sign-off block
- Work Plan — 11 phases, with two flagged warnings (see §5)
- Day-by-Day Plan — 67 working days, Mon 17 Aug → Tue 17 Nov 2026
- Open Questions — OQ-01…OQ-12, each with the plan-day deadline by which it is needed
- 13 room sections in walk-through order, each with inline photo thumbnails, trade sub-groups, and per-item form fields
- Price Summary — per-room totals, grand total, signatures
- Daily Site Log — 16 rows
- Appendix — 49 pictures at full size, IDs
A01…A50
output/Zonqor Site Checklist.html — same document; the price boxes live-total per room and into the summary table. Built to be self-contained (inline CSS/JS, base64 images, no localStorage).
Conventions that must not drift
- Item IDs are
{ROOM_CODE}-{NN}, assigned in render order after items are grouped by trade.
⚠ Trade grouping reorders items, so an ID is only stable if the data file order and the group order both stay fixed. A previous bug had cross-references pointing at the wrong item because of exactly this — build/verify_refs.py exists to catch it. Run it after every rebuild.
- Picture IDs are
A01…, assigned in room order of first appearance, deduplicated across rooms. - Open Questions are
OQ-01…, and any item blocked by one names it in itsdepends_on. - Assignment is derived from trade, never set per item: gypsum/electrical/plumbing/tiling →
Roderick; fittings/carpentry → ULF.
4. Rooms — walk-through order
Kurt specified this order explicitly; it follows a physical walk through the flat. Keep it.
| # | Code | Room | Items | Notes |
|---|---|---|---|---|
| 1 | WHS | Whole House | 5 | Cross-room items only, priced once |
| 2 | FBL | Front Balcony | 10 | Outdoor kitchen lives here (Kurt confirmed) |
| 3 | OPN | Open Plan — Kitchen → Dining → Living | 14 | One space; items carry zone tags |
| 4 | ENT | Entrance | 2 | Electrical central unit + intercom |
| 5 | MCR | Main Corridor | 6 | |
| 6 | SCR | Small Corridor | 1 | |
| 7 | MBR | Main Bedroom | 12 | |
| 8 | OFF | Study / Office | 9 | Sits under/off the Main Bedroom (Kurt) |
| 9 | ENS | Ensuite | 5 | |
| 10 | BTH | Main Bathroom | 6 | |
| 11 | CYB | Cy's Bedroom (with window) | 5 | |
| 12 | RAF | Rafe's Bedroom (with back balcony door) | 5 | |
| 13 | BBL | Back Balcony | 1 | Scope not yet established |
Full item list: data/rooms.json (structured) and data/items.csv (flat).
5. The work plan, and the two things worth arguing about
Roderick proposed this order: clean up old parquet / demolish the dining feature wall → plumbing / core electricity + electrical unit / ethernet → cleanup with skip → tiling → gypsum amendments → "let's see what's missing after this".
Six stages were missing and were added: painting, second-fix electrical (fitting and terminating every point, hanging fittings, testing and certification), carpentry, fittings and sanitary, the outdoor works, and snagging/handover.
One sequencing change was proposed and is NOT yet agreed with Roderick. He put tiling before gypsum. That is right for patching, but wrong for three full walls — the study partition (OFF-01), the small corridor extension (SCR-01) and the sofa wall (OPN-02). Tiling under a wall that is then built on top wastes 120×120 tile and seats the wall on tile rather than slab. The plan therefore splits it: Phase 4 builds partitions before tiling; Phase 6 keeps patching after. ⚠ Flag this to Roderick on day one. He may have a reason.
| Ph | Phase | Days |
|---|---|---|
| 1 | Strip-out & demolition | 5 |
| 2 | First fix — plumbing, electrical, ethernet, gas | 16 |
| 3 | Clear site — skip | 1 |
| 4 | Structural gypsum (partitions) — moved before tiling | 6 |
| 5 | Tiling | 16 |
| 6 | Gypsum amendments & making good | 2 |
| 7 | Painting | 5 |
| 8 | Second fix electrical | 6 |
| 9 | Carpentry — ULF (+ Roderick cut-ins) | 3 |
| 10 | Fittings, sanitary & outdoor | 5 |
| 11 | Snagging & handover | 2 |
Duration assumptions (all stated in the document, all unconfirmed): 2 people, Mon–Fri, no public holidays deducted; painting assumed to be in Roderick's price — if it is not, Phase 7 is a separate trade and every date after it shifts; ULF's carpentry shown sequentially but could start earlier and pull the finish in by ~1 week; balcony tiling assumes dry weather.
6. Open questions — all still unanswered
None of these have been decided. Deadlines are plan-days from the 17 Aug start.
| ID | Question | Room | Needed by |
|---|---|---|---|
| OQ-01 | Outdoor wall finish — PVC sheet or tiles? (one or the other, not both) | FBL | day 63 |
| OQ-02 | Outdoor kitchen — design / equipment spec | FBL | day 18 |
| OQ-03 | New consumer unit — location and capacity | ENT | day 6 |
| OQ-04 | Study/Office plugs — quantity | OFF | day 14 |
| OQ-05 | Ensuite tiles — confirm 60×30 dimensions | ENS | day 42 |
| OQ-06 | Living/Dining TV wall — PVC sheet spec | OPN | day 64 |
| OQ-07 | Sofa-area TV unit — electrical point spec | OPN | day 10 |
| OQ-08 | Intercom — reuse or replace? audio or video? (changes first-fix cable) | ENT | day 8 |
| OQ-09 | Who books and pays for the skips? | WHS | day 5 |
| OQ-10 | Tiling scope — photos show ensuite, main bathroom and front balcony are already tiled. Does ~145 sqm still stand, and what is in it? | WHS | before pricing |
| OQ-11 | Fireplace — photo shows it installed with the wall already opened beneath. Remove, or keep and rework around it? | OPN | day 4 |
| OQ-12 | Two tile formats on site: Fanal 120 and Fanal 31.6×90. What is the 31.6×90 for? | WHS | day 29 |
OQ-10 is the commercially significant one and should be settled before any price is agreed.
7. Source material — what exists and how good it is
images/measurements/ — the dimensional evidence (full resolution, read these to build a plan)
openplan_dimensions_1/2/3.jpg— open-plan elevations with mm dimensionsmain_bedroom_dimensions_1/2.jpgkids_bedroom_dimensions_1/2.jpgcorridor_dimensions_1.jpgtiles_sq_m.jpg— floor plan of the whole flat with "125 sqm" marked. The closest thing to a
plan that exists. Roderick's quote says 145 sqm; Kurt chose 145. The discrepancy is unexplained and is now folded into OQ-10.
Copy_of__furniture_+_gypsum_measurement_p1.jpg— contains the study partition spec:
*"Sound proof gypsum partition to consist of: 1. Inside of study to have 2 layers of boards.
- Middle part to have a profile of 75 mm filled with rockwool insulation. 3. Outer part of study
to have a single board." Also notes "wardrobe around ensuite door for maximum storage"*.
furniture_layout_ELEVATIONS_p1.jpg— the only elevation sheet the designer producedbalcony_plan_p1.jpg,kids_bedroom_only_p1.jpg,study_and_bedroom_revised_p1.jpg,
Copy_of_kids_bedroom_measurement_opt4_p1.jpg
images/design/ — designer renders (1400px, visual intent only, no dimensions)
19 renders: main bedroom (4), study (3), Cy (2), Rafe (2), corridors (2), balcony (2), dining feature wall, dining+TV unit, dining+kitchen, sofa.
images/site/ — 75 photos Kurt took on site, Aug 2026
IMG_9826–IMG_9896 are photos; IMG_9897–IMG_9900_frame are stills pulled from 4 videos. Originals were HEIC (135 MB) and were converted to 1100px JPEG. The originals are still on Kurt's Mac at ~/Desktop/HomeWorks&ReDesign/Site Photos/_Unsorted/Zonqor Works/ if higher resolution is ever needed.
Only 21 of the 75 are placed into the document. See docs/05-photo-index.md for the full inventory including which are placed, which are identified but unplaced, and which are unidentified. 54 remain unassigned — mostly close-ups of switch plates and sockets that look identical across rooms. Kurt was asked for the order he walked the flat so they could be assigned by number range; he has not yet answered. That question is still open and is the fastest way to place them.
data/
works_Sheet1.csv— the original room-by-room work list fromZonqor 2025 - Works.xlsx
(126 rows × 20 cols): # / NEW-EXIST / ACTION / ROOM / LOCATION / DESCRIPTION / FOR / Gypsum-paint works / Furniture Setup / Electrical Points / Light Points / Switch Points / Water Points / Gas Points / IMAGE. This is the origin of the whole checklist.
works_Sheet2.csv— Roderick's cost estimate, including the "145msq" tiling figurelights_plugs_to_buy.csv— shopping list of switches/plugs/lights per room, mirror sizeslights_plugs_needed_for_Roderick_-ULF.csv— room-by-room wiring notes; the source for details
like the ethernet behind the sofa, the LED bar near the office door, the emergency-light short circuit in the back corridor, and "unit for salva vita"
pdf_text/— extracted text from each measurement PDF (thin; the PDFs are drawings)rooms.json,items.csv— the current 81 items, machine-readable
8. What the site photos revealed — and why it matters
The photos changed the picture materially, and these findings are not yet resolved:
- The flat is in far better condition than "refurbishment" implies. The ensuite and main
bathroom are finished in large-format travertine and grey marble, with shower glass, tray and sanitary ware installed. The front balcony is already tiled. This is a refresh, not a strip-out.
- Therefore the tiling quantity is probably wrong. "120×120 full house including balconies,
~145 sqm" almost certainly applies only to the parquet areas — living/dining, corridors, bedrooms. → OQ-10, and it moves both price and the 16-day tiling phase.
- The fireplace appears newly installed (
IMG_9893), yet the wall beneath it is already broken
open. The item says "Dining Feature Wall + Fireplace Removal". → OQ-11.
- Two tile formats are on site: Fanal 120 boxes in the open plan, Fanal 31.6×90 stacked on the
balcony. The work list only knows about 120×120 and the ensuite 60×30. → OQ-12.
IMG_9845shows the existing consumer unit, its position by the entrance door, and an
adjacent thermostat panel — directly useful for OQ-03 and for any electrical plan.
- Materials already delivered: Fanal tiles (both formats), Isomat AK22 tile adhesive, gypsum board.
9. Files in this package
The handoff bundle has been unpacked to the working-folder root, so this file loads automatically when Claude Code starts in ~/Desktop/ZonqorHome-RefurbWorks. Every relative path below is unchanged from the bundle.
ZonqorHome-RefurbWorks/ ← project root, run claude here ├── CLAUDE.md ← you are here ├── README.md ← how to pick this up ├── docs/ │ ├── 01-decisions-log.md every decision Kurt made, verbatim, with what it changed │ ├── 02-open-questions.md OQ-01..12 in full, with why each matters │ ├── 03-work-plan.md phases, durations, assumptions, the sequencing argument │ ├── 04-day-by-day.md all 67 days with dates and item IDs │ ├── 05-photo-index.md all 75 site photos: what each shows, placed or not │ ├── 06-source-data.md what is in each source file and how reliable it is │ ├── 07-site-observations.md the discrepancies found in the photos, in detail │ ├── 08-whats-missing.md the brief for the architecture + electrical plan work │ ├── 09-base-plan.md how the base plan was reconstructed, and how far to trust it │ └── 10-project-index.md what index.html is, and the three constraints on it ├── data/ rooms.json, items.csv, works_*.csv, lights_plugs_*.csv, pdf_text/ ├── images/ measurements/ (dimensional, full-res) · design/ · site/ ├── index.html ← LOCAL PROJECT BROWSER. Double-click it. Built by build/build_index.py │ also hosted: https://zonqor-refurb-c7cc97.pages.dev (public link, no login) ├── build/ checklist: build_v3.py, rooms_data2.py, verify_refs.py, pdfcheck.py, gen_day_plan.py │ base plan: plan_data.py, build_plan.py, pdfplan.py │ index: build_index.py, deploy.sh (Cloudflare Pages — the only online step) ├── output/ Zonqor Site Checklist.pdf + .html, Zonqor Base Plan.pdf, Photo Sort Sheet.pdf │ preview/ 96 dpi page JPEGs, generated for index.html — safe to delete └── _archive/ zonqor-refurb-handoff.zip (the original bundle) + an earlier PDF export
_archive/ is the untouched original. Nothing reads from it — it exists so the flattening is reversible and so the pre-handoff PDF export is not lost.
10. Rebuilding the checklist
cd build python3 build_v3.py # writes ../output/renovation_checklist.html python3 verify_refs.py # MUST pass: no broken refs, no duplicate IDs python3 gen_day_plan.py # rewrites ../docs/04-day-by-day.md from the built HTML python3 pdfcheck.py # renders ../output/Zonqor Site Checklist.pdf via Playwright
The schedule has exactly one source of truth: START and DAYS in build_v3.py. Change the start date there and nowhere else — the cover, the phase table, the day plan and docs/04-day-by-day.md all derive from it. (Before 16 Aug 2026 the start date was also hardcoded as a literal string in three places in the HTML; those are now derived, and the markdown day plan was hand-maintained until gen_day_plan.py replaced it.) Dates stated in prose — CLAUDE.md, docs/03-work-plan.md, docs/07-site-observations.md — are still manual; grep for the old date after any change.
⚠ Two filenames, one document. build_v3.py writes output/renovation_checklist.html; the file delivered to Kurt is output/Zonqor Site Checklist.html — a renamed copy. pdfcheck.py reads the build name and writes the delivered PDF name. After a rebuild you must copy the HTML across yourself if the HTML is being handed over:
cp ../output/renovation_checklist.html "../output/Zonqor Site Checklist.html"
index.html must be rebuilt after any of this — it embeds the docs and data at build time:
cd build && python3 build_index.py
The base plan is a separate, smaller pipeline (added 16 Aug 2026):
cd build python3 build_plan.py # geometry from plan_data.py -> ../output/base_plan.html python3 pdfplan.py # renders ../output/Zonqor Base Plan.pdf
It shares the honesty discipline and the print-first CSS but nothing else — it has its own data file and no cross-reference verifier, because it has no IDs to break. See docs/09-base-plan.md.
Then look at the pages. pdf2image is not installed on this Mac, but poppler is, so render directly and read the PNGs — never assume the CSS worked:
pdftoppm -png -r 90 -f 1 -l 4 "../output/Zonqor Site Checklist.pdf" /tmp/pg
Dependencies — what is actually needed. requirements.txt lists the tools used to extract data/ from Kurt's spreadsheets and PDFs. The rebuild itself needs far less:
| Needed to rebuild | Status on this Mac |
|---|---|
build_v3.py — Python stdlib only | ✅ Python 3.14.4 at /opt/homebrew/bin/python3 |
verify_refs.py — beautifulsoup4 | ✅ installed |
pdfcheck.py — playwright + chromium | ✅ installed (~/Library/Caches/ms-playwright/chromium-1208) |
visual check — poppler pdftoppm | ✅ /opt/homebrew/bin/pdftoppm |
pandas, pdfplumber, pdf2image | ❌ not installed — only needed to re-extract source data |
Verified 16 Aug 2026: a clean rebuild on this Mac reproduces Zonqor Site Checklist.html byte-for-byte (same MD5), and the PDF renders to the same 23 pages, laid out correctly. The HTML generator is deterministic — so any HTML diff you see later is a real content change, not build noise. If a rebuild ever stops matching unexpectedly, that is a signal, not a nuisance.
⚠ The PDF is not byte-reproducible. pdfcheck.py overwrites Zonqor Site Checklist.pdf, and this Mac's Chromium emits a different byte stream (~2.9 MB) than the sandbox Chromium that made the shipped file (~2.4 MB) — same 23 pages, same layout. So compare PDFs by looking at pages, not by MD5, and be aware that running pdfcheck.py replaces the file Christine may be printing from. The original shipped PDF is preserved inside _archive/zonqor-refurb-handoff.zip.
build_v3.py reads rooms_data2.py (the item data) and emits one self-contained HTML file. Room content lives in rooms_data2.py; layout, the day plan and the narrative warnings live in build_v3.py.
11. Working notes for whoever picks this up
- Kurt corrects fast and specifically. He sends annotated screenshots. When he says something is
wrong, change exactly that and re-verify — do not rebuild around it.
- He values being told when something is wrong, e.g. the gypsum-before-tiling argument and the
tiling-scope discrepancy were both welcomed. Flag problems rather than quietly complying.
- Do not invent detail to fill a gap. Several times the honest answer was "no coordinate data
exists, so this is indicative only" — that framing is in the document and should stay.
- Print output must be verified by actually rendering the PDF and looking at the pages, not by
assuming the CSS worked. pdfcheck.py + pdf2image is the loop that has been used throughout.
README.md
Zonqor Refurbishment — Claude Code handoff
Everything from the Cowork session that produced the site checklist, packaged so a fresh Claude Code session can carry on without losing anything.
Start here
- Open
index.html(double-click it). A local browser for everything in this folder —
deliverables, docs, data, all 109 images. No internet needed. See docs/10-project-index.md. Also hosted at https://zonqor-refurb-c7cc97.pages.dev (public to anyone with the link — redeploy with ./build/deploy.sh).
- Read
CLAUDE.md— full project context, conventions, constraints and current state. - Read
docs/08-whats-missing.md— the brief for what still needs building
(architecture plan, electrical plan, elevations).
- Look at
images/measurements/before designing anything. Those are the only
dimensioned drawings that exist. tiles_sq_m.jpg is the closest thing to a floor plan.
The one-paragraph version
Kurt is refurbishing his Zonqor apartment. Roderick (+ son) start Mon 17 Aug 2026; ULF does carpentry. A printed A4 checklist of 81 work items across 13 rooms exists and works — each item has an ID, trade, assignee, dependency, phase, and paper fields for status/date/agreed price. What does not exist: a floor plan, an electrical plan, or elevations. The designer supplied renders but no technical drawings. Twelve open questions are unanswered, two of them urgent (fireplace: remove or keep, by day 4; tiling scope: the bathrooms and balcony turn out to be already tiled, which likely makes the 145 sqm quote wrong).
Ground rules carried over
- Print is the primary medium. Christine works from paper on site at night.
- Never fake technical certainty. No coordinate data exists for electrical points; anything
drawn is a proposal to confirm on site, and must be labelled that way.
- Run
build/verify_refs.pyafter every rebuild. Item IDs are positional and a reordering
silently breaks cross-references. This has already happened once.
Layout
| Path | What |
|---|---|
index.html | Local browser for the whole project — start here. Rebuild with build/build_index.py |
CLAUDE.md | Master context. Read first. |
docs/ | Decisions log, open questions, work plan, day-by-day, photo index, source notes, site observations, the brief for what's missing, and how the base plan was reconstructed |
data/ | rooms.json + items.csv (81 items), original spreadsheets as CSV, PDF text |
images/measurements/ | Dimensioned drawings, full resolution — the design source of truth |
images/design/ | 19 designer renders (visual intent only) |
images/site/ | 75 site photos, Aug 2026 |
build/ | Generator scripts |
output/ | The deliverables: Zonqor Site Checklist (PDF + HTML) and Zonqor Base Plan (PDF) |
Rebuild
cd build && python3 build_v3.py && python3 verify_refs.py && python3 pdfcheck.py pdftoppm -png -r 90 -f 1 -l 4 "../output/Zonqor Site Checklist.pdf" /tmp/pg # then look at the pages
Verified working on Kurt's Mac (16 Aug 2026) — a clean rebuild reproduces the shipped HTML byte-for-byte. Note the build writes output/renovation_checklist.html; the delivered HTML is a renamed copy. See CLAUDE.md §10.
01-decisions-log.md
Decisions log
Every decision Kurt made during the Cowork session, in order, with what it changed. Quoted text is his own wording. Anything not listed here is not decided.
Scope and content
| # | Decision | What it changed |
|---|---|---|
| 1 | Tiles: 120×120 full house including balconies, except bathrooms; ensuite 60×30 (dimensions still to confirm) | WHS-05, ENS-04. The 60×30 confirmation is still open as OQ-05. |
| 2 | Known fault: "Kitchen Appliances Oven when switched cuts salvavita" | OPN-05, tagged as a known issue and made dependent on the new consumer unit |
| 3 | Consumer unit: "Full refresh to unit / panel" (not a repair) | ENT-01 |
| 4 | Tiling area: 145 sq m (Roderick's quote), chosen over the 125 sqm marked on the designer's floor plan | WHS-05. ⚠ Now in question — see OQ-10. |
| 5 | Outdoor wall: "this will be PVC / TILES - same thing - one of them to be done - still to be decided" | Merged two duplicate items into one FBL-10, and created the first Open Question |
| 6 | The dining item is to be called "Dining Feature Wall + Fireplace Removal" | OPN-01 |
| 7 | "Building Wardrobe is on ULF" | MBR-11 moved from Roderick to ULF |
Structure and format
| # | Decision | What it changed |
|---|---|---|
| 8 | "put into sections (electrical / tile fitting / plumbing) per room" | Every room's items grouped by trade in a fixed order: Gypsum → Electrical → Plumbing → Tiling → Fittings |
| 9 | "Add an Assigned Section and Dependency in every item. Gypsum / Tiles / Electrical / Plumbing = Roderick. Carpentry = ULF." | The assignment rule. Derived from trade, never set per item. |
| 10 | "make the what (title / type / text) to the left and project management part to the right … and make it expandable" | Two-column item layout |
| 11 | "Can you give them an ID Number for Easy Reference - Ideally Code-ID by room" | The {ROOM}-{NN} ID scheme |
| 12 | Print-first rebuild: "this will be printed from a PDF and my wife Christine will check-in with the worker and update the checklist per item and comments during the night + agree on prices per item" | The whole document was rebuilt around paper: status/date/price/initials/comments fields, price summary, daily log, sign-off blocks |
| 13 | "make it one document and compact. we will have appendix for the pictures and reference them with an ID" | 34 pages → 15. Pictures moved to an ID-referenced appendix (A01…) |
| 14 | "still have thumbnails inline per room. just larger same photos in the appendix" | Small inline thumbnails restored, full size kept in the appendix |
Room order and layout (asked explicitly, answered explicitly)
| # | Question asked | Kurt's answer |
|---|---|---|
| 15 | Room order | Front Balcony → Open Plan (Kitchen→Dining→Living) → Entrance → Main Corridor → Small Corridor → Main Bedroom → Ensuite → Main Bathroom → Cy Bedroom (with Window) → Rafe Bedroom (with Back Balcony Door) → Back Balcony |
| 16 | Where does the Study/Office sit? | "Do it under Main Bedroom" → placed immediately after Main Bedroom |
| 17 | Which balcony gets the outdoor kitchen? | Front Balcony → all BBQ/gas/waterproof-plug items to FBL |
| 18 | What is in the Entrance? | "Electrical Central Unit / Intercom" → the consumer unit was moved from the General section to ENT, and an intercom item was added |
| 19 | How should the day plan be dated? | Real dates, 5-day week |
| 20 | Start date | Thursday 13 August 2026 (sent mid-turn) |
Work order
| # | Decision | Status |
|---|---|---|
| 21 | Roderick's proposed order: parquet clean-up / demolish feature wall → plumbing + core electricity + electrical unit + ethernet → cleanup with skip → tiling → gypsum amendments → "lets try to see whats missing after this" | Adopted as phases 1–6, with six further phases added |
| 22 | Kurt asked for a day-by-day turnkey plan for Roderick + son | Built: 67 working days, originally Thu 13 Aug → Fri 13 Nov 2026 |
| 23 | Start delayed — the worker was not available on 13 Aug. New start Mon 17 Aug 2026 (Kurt, 16 Aug) | Whole plan re-dated: 67 working days, Mon 17 Aug → Tue 17 Nov 2026. Durations, phase order and plan-day numbers unchanged; every calendar date moved two working days later. Open-question deadlines are expressed as plan-days, so they still hold — but they now fall two working days later. |
Decisions NOT made
- All twelve open questions (see
02-open-questions.md) - The gypsum-before-tiling sequencing change — proposed by the previous session, not yet put to Roderick
- The order Kurt walked the flat when photographing — asked for, never answered; blocks placing 54 photos
02-open-questions.md
Open questions
None of these are answered. Deadlines are plan-days counted from the 17 Aug 2026 start (delayed from 13 Aug — the plan-day numbers are unchanged, the calendar dates moved). Any work item blocked by one names it in its depends_on field.
OQ-01 — Outdoor wall finish — PVC sheet or tiles?
Room: Front Balcony · Needed by: day 63
One or the other, never both. Kurt merged two duplicate items over this. Blocks FBL-10.
OQ-02 — Outdoor kitchen — design / equipment spec
Room: Front Balcony · Needed by: day 18
Blocks five FBL items including the gas line. Nothing outdoor can be ordered until this lands.
OQ-03 — New consumer unit — location and capacity
Room: Entrance · Needed by: day 6
ENT-01 is a full board replacement and everything electrical downstream waits on it. IMG_9845 shows the existing unit and its position.
OQ-04 — Study/Office plugs — quantity
Room: Study / Office · Needed by: day 14
Quantity was never specified. Blocks OFF-04.
OQ-05 — Ensuite tiles — confirm 60×30 dimensions
Room: Ensuite · Needed by: day 42
Now entangled with OQ-10: photos show the ensuite is already tiled, so the prior question is whether it is re-tiled at all.
OQ-06 — Living/Dining TV wall — PVC sheet spec (material, size, fixing)
Room: Open Plan · Needed by: day 64
Was a dangling reference with no question behind it; formalised.
OQ-07 — Sofa-area TV unit — electrical point spec
Room: Open Plan · Needed by: day 10
Needed before first fix reaches the living zone.
OQ-08 — Intercom — reuse or replace? audio or video?
Room: Entrance · Needed by: day 8
Changes the cable pulled at first fix, so it cannot wait. Added when Kurt defined the Entrance as 'Electrical Central Unit / Intercom'.
OQ-09 — Who books and pays for the skips?
Room: Whole House · Needed by: day 5
Two skips assumed: after demolition, and after first fix.
OQ-10 — Tiling scope — site photos show the ensuite, main bathroom and front balcony are already tiled. Does ~145 sqm still stand, and what is in it?
Room: Whole House · Needed by: before pricing
The commercially significant one. Affects the price and the 16-day tiling phase.
Partly answered, 16 Aug 2026 — see 09-base-plan.md. Measuring the reconstructed base plan shows the 125 vs 145 sqm gap is the two balconies: internal rooms less the two wet rooms come to 122.7 sqm against the designer's 125, and adding the balconies (21.6 sqm) lands on Roderick's
- The two figures were never in conflict — 125 is the internal floor excluding wet rooms, 145
is everything including balconies.
What is still open is the part that costs money: how much is actually being tiled? The photos show the ensuite, main bathroom and front balcony are already finished. If none of those are re-tiled the new 120×120 area is nearer 123 sqm than 145 — roughly 22 sqm of difference to settle with Roderick before agreeing a price.
OQ-11 — Fireplace — the photo shows it installed with the wall already opened beneath. Remove, or keep and rework around it?
Room: Open Plan · Needed by: day 4
OPN-01 says 'removal' but IMG_9893 shows a new-looking fireplace. Demolition day depends on the answer.
OQ-12 — Two tile formats on site: Fanal 120 and Fanal 31.6×90. What is the 31.6×90 for?
Room: Whole House · Needed by: day 29
Not in the work list at all. Seen in IMG_9894.
Priority
- OQ-10 — settle before agreeing any price.
- OQ-11 — needed by day 4, changes what happens on demolition day.
- OQ-03 and OQ-08 — week one, both affect first fix.
- OQ-09 — day 5.
- The rest follow the plan dates.
03-work-plan.md
Work plan
Start Mon 17 Aug 2026 · 67 working days, Mon–Fri · finish Tue 17 Nov 2026 · Roderick + son (2 people).
Delayed from the original Thu 13 Aug start (contractor unavailable). Durations and phase order are unchanged; every date shifted by two working days.
Origin
Roderick proposed: clean up old parquet / demolish the dining feature wall → plumbing / core electricity + electrical unit / ethernet → cleanup with skip → tiling → gypsum amendments → "lets try to see whats missing after this".
Six stages were missing and were added: painting, second fix electrical, carpentry, fittings and sanitary, the outdoor works, and snagging and handover.
Four work items were also missing and were added: WHS-01 strip out the parquet, WHS-04 level the screed once it is up, WHS-02 skip hire, and ENS-03 tank the ensuite wet area before tiling.
⚠ The one open argument — gypsum before tiling, not after
Roderick's order puts tiling before gypsum. That is correct for patching and making good, but wrong for three full walls: the study partition (OFF-01), the small corridor extension (SCR-01) and the sofa wall (OPN-02). Tiling under a wall that is then built on top wastes 120×120 tile and seats the wall on tile instead of slab.
The plan therefore splits it — Phase 4 builds partitions before tiling, Phase 6 keeps patching after. This has not been put to Roderick. Raise it on day one; he may have a reason.
Phases
| Ph | Phase | Days | Notes |
|---|---|---|---|
| 1 | Strip-out & demolition | 5 | Parquet up, feature wall and fireplace down, old fittings removed. |
| 2 | First fix — plumbing, electrical, ethernet, gas | 16 | Consumer unit, chasing, all cable and pipe runs, gas line. Nothing gets closed up until this is signed off. |
| 3 | Clear site — skip | 1 | Rubble and chasing debris out before tiling. |
| 4 | Structural gypsum (partitions) | 6 | MOVED BEFORE TILING — see the argument below. |
| 5 | Tiling | 16 | Screed levelled, wet areas tanked, 120x120 through the flat, ensuite 60x30. |
| 6 | Gypsum amendments & making good | 2 | Patching at tile/wall junctions. |
| 7 | Painting | 5 | Ceilings and walls, including the ensuite ceiling. |
| 8 | Second fix electrical | 6 | Fit and terminate every point, hang fittings, LED strips and sensors, then test and certify. |
| 9 | Carpentry — ULF (+ Roderick cut-ins) | 3 | Wardrobes, cabinets, TV unit. Roderick returns to cut in gyser/pump and ensuite switches. |
| 10 | Fittings, sanitary & outdoor | 5 | Mirrors, shower glass and tray, sanitary ware, vent covers, PVC/tile outdoor finish. |
| 11 | Snagging & handover | 2 | Walk the flat against the checklist, fix defects, final clean, hand over certificates. |
Assumptions — all stated, none confirmed
- Two people on site, Mon–Fri. No public holidays deducted.
- Painting is assumed to be in Roderick's price. If it is not, Phase 7 is a separate trade and every date after it shifts.
- ULF's carpentry is shown sequentially but could start earlier once rooms are tiled and painted — that would pull the finish in by roughly a week.
- Balcony tiling assumes dry weather.
- The 16-day tiling phase assumes the full ~145 sqm. If OQ-10 reduces the area, this shortens.
- Nothing is committed until Roderick agrees it.
04-day-by-day.md
Day-by-day plan
Mon 17 Aug 2026 → Tue 17 Nov 2026 · 67 working days, Mon–Fri. Roderick + son unless noted. Generated by build/gen_day_plan.py from the built checklist — regenerate rather than hand-edit.
Phase 1 — Strip-out & demolition
| Day | Date | Work | Items |
|---|---|---|---|
| 1 | Mon 17 Aug | Site set-up: protect what stays, isolate services. Start lifting parquet — Open Plan. | WHS-01 |
| 2 | Tue 18 Aug | Continue parquet lift — bedrooms and corridors. | WHS-01 |
| 3 | Wed 19 Aug | Finish parquet lift. Strip out old ceiling lights, bedside lamps and window plugs. | WHS-01, MBR-02, MBR-03, MBR-04 |
| 4 | Thu 20 Aug | Demolish dining feature wall + fireplace. Check for live services in the wall first. | OPN-01 |
| 5 | Fri 21 Aug | Clear demolition debris, first skip out. Walk the back balcony and write in its scope. | WHS-02, BBL-01 |
Phase 2 — First fix — plumbing, electrical, ethernet, gas
| Day | Date | Work | Items |
|---|---|---|---|
| 6 | Mon 24 Aug | Set out and confirm the new consumer unit position at the entrance (OQ-03 must be answered). | ENT-01 |
| 7 | Tue 25 Aug | Install consumer unit, terminate mains. Nothing else energises until this is in. | ENT-01 |
| 8 | Wed 26 Aug | Chase entrance and corridors. Pull intercom cable. | ENT-02, MCR-01, MCR-02 |
| 9 | Thu 27 Aug | Open Plan kitchen zone — ceiling lights, hood, dedicated oven circuit. | OPN-03, OPN-04, OPN-05 |
| 10 | Fri 28 Aug | Open Plan dining/living — cabinet lights, rope light, switches, plugs, TV point. | OPN-06 to OPN-10 |
| 11 | Mon 31 Aug | Ethernet runs back to the entrance unit. Relocate double-pole switch. | OPN-11, OPN-12, OFF-08 |
| 12 | Tue 01 Sep | Kitchen sink onto main feed, RO connection left ready. | OPN-13 |
| 13 | Wed 02 Sep | Main bedroom first fix — ceiling, rope, bedside, vanity, wardrobe LED. | MBR-05 to MBR-09 |
| 14 | Thu 03 Sep | Study/office first fix — ceiling, under-desk LED, plugs, switches, LED bar. | OFF-02 to OFF-07 |
| 15 | Fri 04 Sep | Cy's bedroom first fix. | CYB-01 to CYB-05 |
| 16 | Mon 07 Sep | Rafe's bedroom first fix. | RAF-01 to RAF-05 |
| 17 | Tue 08 Sep | Main bathroom — gyser position, pipework and point. Ensuite switch route. | BTH-01 |
| 18 | Wed 09 Sep | Front balcony — side lights, floor lights, switches, waterproof plugs, camera point. | FBL-01 to FBL-08 |
| 19 | Thu 10 Sep | Front balcony gas line + pressure test and certificate. | FBL-09 |
| 20 | Fri 11 Sep | Corridor emergency light short circuit. Test all existing feeds. | MCR-03, MCR-04, WHS-03 |
| 21 | Mon 14 Sep | First-fix sign-off walk. Photograph every open chase before anything is closed up. | all first fix |
Phase 3 — Clear site — skip
| Day | Date | Work | Items |
|---|---|---|---|
| 22 | Tue 15 Sep | Second skip — chasing debris out, sweep, floors prepped for tiling. | WHS-02 |
Phase 4 — Structural gypsum (partitions)
| Day | Date | Work | Items |
|---|---|---|---|
| 23 | Wed 16 Sep | Set out and frame the study partition. | OFF-01 |
| 24 | Thu 17 Sep | Study partition — rockwool insulation and inner two layers of board. | OFF-01 |
| 25 | Fri 18 Sep | Study partition — outer board, tape and joint. | OFF-01 |
| 26 | Mon 21 Sep | Small corridor partition extended +10cm for the new cabinets. | SCR-01 |
| 27 | Tue 22 Sep | Gypsum behind sofa (250mm x 1m) including the rope-light housing. | OPN-02 |
| 28 | Wed 23 Sep | Main bedroom — close off old ceiling light area. | MBR-01 |
Phase 5 — Tiling
| Day | Date | Work | Items |
|---|---|---|---|
| 29 | Thu 24 Sep | Screed check and levelling — open plan. | WHS-04 |
| 30 | Fri 25 Sep | Screed levelling — bedrooms and corridors. | WHS-04 |
| 31 | Mon 28 Sep | Tank ensuite floor and shower walls. Must be done before any ensuite tile. | ENS-03 |
| 32 | Tue 29 Sep | Set out 120x120 in the open plan — agree the starting line and cut positions with Christine. | WHS-05 |
| 33 | Wed 30 Sep | Tile open plan — living/dining. | WHS-05 |
| 34 | Thu 01 Oct | Tile open plan — kitchen end. | WHS-05 |
| 35 | Fri 02 Oct | Tile entrance and main corridor. | WHS-05 |
| 36 | Mon 05 Oct | Tile main corridor and small corridor. | WHS-05 |
| 37 | Tue 06 Oct | Tile main bedroom. | WHS-05 |
| 38 | Wed 07 Oct | Tile study and Cy's bedroom. | WHS-05 |
| 39 | Thu 08 Oct | Tile Rafe's bedroom. | WHS-05 |
| 40 | Fri 09 Oct | Tile front balcony — weather permitting. | WHS-05 |
| 41 | Mon 12 Oct | Tile back balcony — weather permitting. | WHS-05 |
| 42 | Tue 13 Oct | Ensuite 60x30 tiling — walls. | ENS-04 |
| 43 | Wed 14 Oct | Ensuite tiling — floor, then grout. | ENS-04 |
| 44 | Thu 15 Oct | Grout throughout, clean down, protect finished floors. | WHS-05 |
Phase 6 — Gypsum amendments & making good
| Day | Date | Work | Items |
|---|---|---|---|
| 45 | Fri 16 Oct | Patching and making good at all tile/wall junctions. | — |
| 46 | Mon 19 Oct | Finish patching, sand, prep every surface for paint. | — |
Phase 7 — Painting
| Day | Date | Work | Items |
|---|---|---|---|
| 47 | Tue 20 Oct | Prime and seal all new gypsum. | — |
| 48 | Wed 21 Oct | Ceilings — bedrooms and study. | — |
| 49 | Thu 22 Oct | Ceilings — open plan and corridors. Ensuite ceiling. | ENS-01 |
| 50 | Fri 23 Oct | Walls — first coat throughout. | — |
| 51 | Mon 26 Oct | Walls — second coat and cutting in. | — |
Phase 8 — Second fix electrical
| Day | Date | Work | Items |
|---|---|---|---|
| 52 | Tue 27 Oct | Fit sockets and switches — open plan and entrance. Intercom unit on. | ENT-02 |
| 53 | Wed 28 Oct | Fit sockets and switches — corridors and bedrooms. | — |
| 54 | Thu 29 Oct | Hang light fittings — open plan and corridors. | — |
| 55 | Fri 30 Oct | Hang light fittings — bedrooms and study. Rope and LED strips. | — |
| 56 | Mon 02 Nov | Wardrobe LED sensors, under-furniture LED, LED bar at study door. | MBR-09, CYB-03, RAF-03 |
| 57 | Tue 03 Nov | Full circuit test, RCD test, certification. Confirm the oven no longer trips. | ENT-01, OPN-05 |
Phase 9 — Carpentry — ULF (+ Roderick cut-ins)
| Day | Date | Work | Items |
|---|---|---|---|
| 58 | Wed 04 Nov | ULF starts wardrobes — main bedroom. Roderick on call for cut-ins. | MBR-11 |
| 59 | Thu 05 Nov | ULF wardrobes — Cy's and Rafe's bedrooms. | — |
| 60 | Fri 06 Nov | Roderick cuts in gyser/pump switches and the ensuite switch. | MBR-10, ENS-02 |
Phase 10 — Fittings, sanitary & outdoor
| Day | Date | Work | Items |
|---|---|---|---|
| 61 | Mon 09 Nov | Shower glass and tray; ensuite finished. | ENS-05 |
| 62 | Tue 10 Nov | Bathroom sanitary — sink, cabinets, flush accessories, mirror. | BTH-02 to BTH-06 |
| 63 | Wed 11 Nov | Mirrors — corridor 3x1.5m, office 2x2m, main bedroom vanity. | MCR-06, OFF-09, MBR-12 |
| 64 | Thu 12 Nov | Outdoor wall finish — PVC or tiles, whichever OQ-01 decided. | FBL-10 |
| 65 | Fri 13 Nov | Vent covers, PVC sheet on TV wall, remaining fittings. | MCR-05, OPN-14 |
Phase 11 — Snagging & handover
| Day | Date | Work | Items |
|---|---|---|---|
| 66 | Mon 16 Nov | Snagging walk with Christine against this checklist, room by room. | all |
| 67 | Tue 17 Nov | Fix snags, final clean, hand over certificates and warranties. | all |
05-photo-index.md
Photo index — 75 site photos, August 2026
Kurt photographed the flat before works. Originals were HEIC (135 MB) and live on his Mac at ~/Desktop/HomeWorks&ReDesign/Site Photos/_Unsorted/Zonqor Works/. This package carries 1100px JPEGs in images/site/. IMG_9897–IMG_9900 are stills extracted from 4 videos (suffixed _frame).
Status: 21 placed into the document, 54 not. The unplaced ones are mostly close-ups of switch plates and sockets that look identical across rooms. Kurt was asked for the order he walked the flat, so they could be assigned by photo-number range — he has not answered. That is still the fastest route to placing them.
Descriptions below are from visual inspection. Room attributions in the last table are unknown, not guesses to be trusted.
Placed in the document (21)
| Photo | Room | What it shows |
|---|---|---|
IMG_9826 | FBL | front balcony — BBQ corner, existing feed & camera |
IMG_9827 | FBL | front balcony looking out, adhesive stacked |
IMG_9828 | FBL | front balcony — already tiled, membrane at edge |
IMG_9895 | FBL | sliding door to front balcony |
IMG_9834 | OPN | open plan overview — parquet still down, Fanal 120 tiles delivered |
IMG_9835 | OPN | kitchen island, units wrapped |
IMG_9839 | OPN | dining feature wall + fireplace, wall opened below |
IMG_9893 | OPN | fireplace close-up — installed, wall broken beneath |
IMG_9840 | OPN | living — existing socket run |
IMG_9896 | OPN | gypsum board delivered on site |
IMG_9845 | ENT | EXISTING CONSUMER UNIT + thermostat + entrance door — key for OQ-03 |
IMG_9849 | MCR | main corridor looking through |
IMG_9850 | MCR | main corridor — existing cove lighting |
IMG_9843 | MCR | corridor switch position |
IMG_9872 | BTH | main bathroom — existing grey marble finish |
IMG_9873 | BTH | main bathroom — WC & window |
IMG_9875 | BTH | main bathroom — ceiling & downlights |
IMG_9886 | ENS | ensuite — existing shower enclosure |
IMG_9888 | ENS | ensuite — shower tray & glass (to be changed, ENS-05) |
IMG_9889 | ENS | ensuite — WC & window |
IMG_9890 | ENS | ensuite — overview |
Identified but not placed (18)
Room is confident; they were left out to keep the document compact. Add if useful.
| Photo | Likely room | What it shows |
|---|---|---|
IMG_9829 | FBL | balcony exterior wall, sea view |
IMG_9894 | FBL | Fanal 31.6x90 tile boxes stacked on balcony — source of OQ-12 |
IMG_9898f | FBL | video still: balcony with Isomat AK22 adhesive stacked |
IMG_9832 | OPN | feature wall niche/alcove |
IMG_9836 | OPN | corner, skirting, parquet |
IMG_9837 | OPN | wall switch plate |
IMG_9838 | OPN | ceiling downlights + kitchen units |
IMG_9841 | OPN | built-in bench with socket |
IMG_9842 | OPN | bench/shelf run |
IMG_9892 | OPN | recessed hatch with AV/equipment inside |
IMG_9897f | OPN | video still: kitchen island |
IMG_9844 | MCR | corridor/entry area with hooks |
IMG_9864 | MCR | corridor ceiling downlight |
IMG_9885 | ENS | ensuite WC + flush plate |
IMG_9887 | ENS | ensuite ceiling/shower from above |
IMG_9891 | ENS | doorway, taped |
IMG_9874 | BTH | grey marble bathroom, bedding stored in front |
Unidentified (36) — need Kurt's walk order
Bedrooms (MBR / CYB / RAF / OFF) are visually near-identical in close-up: white walls, wood-look floor, wall-mounted AC. Known distinguishers: Cy's has a window, Rafe's has the back balcony door, Main Bedroom has the ensuite door, the Study partition does not exist yet.
| Photo | What it shows |
|---|---|
IMG_9830 | narrow space, two black sockets, tiled floor |
IMG_9831 | corner, tiled floor, white walls |
IMG_9833 | window/door reveal, tiled |
IMG_9846 | bedroom — AC unit, wooden door, switch panel, kitchen visible left |
IMG_9847 | room with ceiling downlights and hooks |
IMG_9848 | partition wall with switch, bed behind |
IMG_9851 | switch plate close-up |
IMG_9852 | socket close-up with cable, skirting |
IMG_9853 | switch plate close-up, taped edge |
IMG_9854 | multi-gang switch, bath/tub visible left |
IMG_9855 | room with window, sockets at skirting |
IMG_9856 | ceiling with hanging cable |
IMG_9857 | socket close-up, taped edge |
IMG_9858 | room corner, double socket, cable |
IMG_9859 | AC unit above window |
IMG_9860 | sockets at skirting, ladder present |
IMG_9861 | corner with socket + extension lead |
IMG_9862 | switch plate, doorway beyond |
IMG_9863 | switch plate close-up |
IMG_9865 | switch plate close-up |
IMG_9866 | floor/wall junction, taped |
IMG_9867 | room corner, two sockets, wood-look floor |
IMG_9868 | door frame taped, socket low |
IMG_9869 | room corner, sockets |
IMG_9870 | bedroom with window, socket |
IMG_9871 | bedroom — AC above window |
IMG_9876 | wall with sockets at skirting |
IMG_9877 | blank wall, single socket |
IMG_9878 | switch plate, opening beyond |
IMG_9879 | ceiling with 3-lamp fitting + downlights, AC, window |
IMG_9880 | bedroom — AC, window, socket |
IMG_9881 | switch + double socket close-up |
IMG_9882 | room corner, switch + socket |
IMG_9883 | wall with switch above triple socket |
IMG_9884 | wall with three sockets in a row |
IMG_9899f | video still: bedroom with window + AC |
IMG_9900f | video still: bedroom with window + AC |
Notable individual photos
IMG_9845— the existing consumer unit, its position by the entrance door, and the adjacent
thermostat panel. Directly relevant to OQ-03 and to designing the new board.
IMG_9893— the fireplace, apparently newly installed, with the wall already broken open
beneath it. The source of OQ-11.
IMG_9894— Fanal 31.6×90 tile boxes on the balcony. A format not in the work list. OQ-12.IMG_9872–9875,IMG_9885–9890— the main bathroom and ensuite, both already finished in
large-format stone. The evidence behind OQ-10 (tiling scope).
IMG_9892— recessed hatch containing AV/equipment; worth identifying for the electrical plan.
06-source-data.md
Source data — what exists and how far to trust it
Spreadsheets
data/works_Sheet1.csv — the origin of the whole checklist
From Zonqor 2025 - Works.xlsx (67 MB, still on Kurt's Mac). 126 rows × 20 columns: # / NEW-EXIST / ACTION / ROOM / LOCATION / DESCRIPTION / FOR / Gypsum-paint works / Furniture Setup / Electrical Points / Light Points / Switch Points / Water Points / Gas Points / IMAGE.
The point-count columns are the only quantitative electrical data that exists. They give counts, never positions. Reliability: good for what and how many, silent on where.
data/works_Sheet2.csv — Roderick's cost estimate
14 rows. Contains the 145 msq tiling figure Kurt chose over the 125 sqm on the designer's plan. Now in question — see OQ-10.
data/lights_plugs_to_buy.csv
Sheet to buy from lights _ plugs _ mirrors.xlsx. Shopping list of switches, plugs and lights per room. Source of the mirror sizes (corridor 3 m × 1.5 m, office 2 m × 2 m) and the wardrobe LED sensors.
data/lights_plugs_needed_for_Roderick_-ULF.csv
Sheet needed for Roderick |ULF. Room-by-room wiring notes. This is where several items came from that are nowhere else: the ethernet behind the sofa, the LED bar near the office door, the emergency-light short circuit in the back corridor, 'unit for salva vita', and the outdoor kitchen/gas/tiles notes. The sheet name is also how it was established that ULF is a person, not an abbreviation.
Drawings
data/pdf_text/ holds the extracted text, but the PDFs are drawings and the text is thin — the rendered images in images/measurements/ are what you actually need to read.
| File | Contains | Use for |
|---|---|---|
tiles_sq_m.jpg | Whole-flat floor plan, '125 sqm' marked | The base plan. Room shapes and adjacencies. |
openplan_dimensions_1/2/3.jpg | Open-plan elevations, mm | Open-plan setting out |
main_bedroom_dimensions_1/2.jpg | Main bedroom, mm | Main bedroom |
kids_bedroom_dimensions_1/2.jpg | Kids' bedrooms, mm | Cy / Rafe |
corridor_dimensions_1.jpg | Corridor, mm | Main corridor |
Copy_of__furniture_+_gypsum_measurement_p1.jpg | Study partition build-up; 'wardrobe around ensuite door' | OFF-01, MBR-11 |
furniture_layout_ELEVATIONS_p1.jpg | The designer's only elevation sheet | Partial elevations |
balcony_plan_p1.jpg | Balcony layout | FBL / BBL |
study_and_bedroom_revised_p1.jpg, kids_bedroom_only_p1.jpg, Copy_of_kids_bedroom_measurement_opt4_p1.jpg | Layout options | Bedrooms |
No dimensioned drawing exists for: Entrance, Small Corridor, Ensuite, Main Bathroom, either balcony, or the Study as a room (only its partition spec).
Verbatim spec worth keeping
Study partition, from the gypsum measurement PDF:
Sound proof gypsum partition to consist of: 1. Inside of study to have 2 layers of boards. 2. Middle part to have a profile of 75 mm filled with rockwool insulation. 3. Outer part of study to have a single board.
Derived data
data/rooms.json and data/items.csv — the current 81 items with IDs, trade, assignee, phase, dependency, tags and notes. Generated from build/rooms_data2.py; regenerate rather than hand-edit.
07-site-observations.md
Site observations — what the photos changed
Kurt photographed the flat in August 2026, before works. Five findings materially affect the plan. None are resolved.
1. The flat is in far better condition than 'refurbishment' implies
The ensuite (IMG_9885–9890) and main bathroom (IMG_9872–9875) are finished in large-format travertine and grey marble, with shower glass, tray and sanitary ware installed. The front balcony (IMG_9828) is already tiled. The parquet is still down in the living/dining, corridors and bedrooms.
This is a refresh, not a strip-out. It affects how the scope should be described to anyone new.
2. The tiling quantity is probably wrong → OQ-10
WHS-05 reads '120×120 full house, including balconies, ~145 sq m'. If the bathrooms and the front balcony are already tiled, the real area is the parquet footprint only — materially less. This moves both the price and the 16-day tiling phase, and it revives the unexplained gap between the 125 sqm marked on the designer's floor plan and the 145 sqm in Roderick's quote.
Settle this before agreeing any price. Computing areas off the base plan may answer it directly.
3. The fireplace looks new, but the wall is already open → OQ-11
IMG_9893 and IMG_9839 show a linear gas fireplace, apparently newly installed, set into the feature wall — with the wall beneath it already broken open. OPN-01 says 'Dining Feature Wall + Fireplace Removal'. Either the fireplace is being torn out, or it is being kept and the wall reworked around it. Demolition is day 4 (Thu 20 Aug), so this cannot wait.
It also raises a second question nobody has answered: has demolition already started?
4. An unexplained second tile format → OQ-12
IMG_9834 shows Fanal 120 boxes in the open plan. IMG_9894 shows Fanal 31.6 × 90 stacked on the balcony. The work list only knows about 120×120 and the ensuite 60×30. Something is on site that nobody has accounted for.
5. The existing consumer unit is documented
IMG_9845 shows the existing board, its position beside the entrance door, and an adjacent thermostat panel. This is the single most useful photo for OQ-03 and for designing the new board (ENT-01).
Materials already delivered
Fanal tiles in both formats, Isomat AK22 tile adhesive (IMG_9827, IMG_9898_frame), gypsum board (IMG_9896). Worth reconciling against a materials schedule, which does not yet exist.
Also seen, not yet explained
IMG_9892— a recessed hatch containing AV/equipment. Relevant to the electrical plan; unidentified.IMG_9841,IMG_9842— built-in bench/shelf runs with sockets, in the open plan.IMG_9850— existing cove/rope lighting in the main corridor, whichMCR-01is meant to match.
08-whats-missing.md
What still needs building
The brief for the Claude Code work. The checklist is done; these are not.
Status, 16 Aug 2026 — item A is done. A reconstructed base plan now exists:output/Zonqor Base Plan.pdf, geometry inbuild/plan_data.py, method and confidence grades indocs/09-base-plan.md. It also produced an answer to OQ-10: the 125 / 145 m² gap is the two balconies. The "measure these on site" list in section A below is still outstanding — the plan is a reconstruction, not a survey, and items B and C have not been started.
A. Architecture / floor plan
Goal. A dimensioned plan of the flat, room by room, good enough for the worker to set out from and for an electrical plan to be drawn on top of.
What you have to work with
| Source | What it gives | Reliability |
|---|---|---|
images/measurements/tiles_sq_m.jpg | The only whole-flat floor plan. Room shapes, relative positions, marked "125 sqm". | Shape: good. Area figure: disputed (quote says 145). Not dimensioned room-by-room. |
openplan_dimensions_1/2/3.jpg | Open-plan elevations with mm dimensions | Good, but elevations not plan |
main_bedroom_dimensions_1/2.jpg | Main bedroom, mm | Good |
kids_bedroom_dimensions_1/2.jpg | Kids' bedrooms, mm | Good |
corridor_dimensions_1.jpg | Corridor, mm | Good |
Copy_of__furniture_+_gypsum_measurement_p1.jpg | Study partition build-up spec; wardrobe-around-ensuite-door note | Good |
furniture_layout_ELEVATIONS_p1.jpg | The designer's only elevation sheet | Partial coverage |
images/site/ (75 photos) | Actual room shapes, window/door positions, existing finishes | Visual only, no scale |
Rooms with no dimensional drawing at all: Entrance, Small Corridor, Ensuite, Main Bathroom, both balconies, Study/Office (only the partition spec, not the room).
Hard constraint. There is no survey. A plan built from this is a reconstruction, and its accuracy varies by room. Label it as such, and mark which dimensions are read from a drawing versus inferred from the plan image. The honest deliverable is a plan with a confidence key, plus a short "measure these on site" list for the gaps.
Suggested first step. Read tiles_sq_m.jpg at full resolution, trace the room outlines and adjacencies, then cross-check each room against its dimension drawing where one exists. That gives a scaled base plan and an explicit list of what could not be verified.
B. Electrical plan
Goal. What the designer never produced. Specifically:
- Consumer unit / distribution board schedule — circuits, ratings, RCD/RCBO arrangement.
ENT-01 is a full replacement, so this is a new board designed from scratch. IMG_9845 shows the existing unit and its position — start there.
- Circuit schedule — which points sit on which circuit. The oven (
OPN-05) trips the current
RCD and is a strong candidate for its own dedicated circuit; that is currently an open item, not a decision.
- Point layout per room — plotted on the base plan from A.
- Cable routes and chases — first fix is 16 days and is the phase everything else waits on.
- Ethernet — several points (
OPN-11,OFF-08,CYB-05,RAF-05) all run back to the
central unit at the entrance. That hub needs designing: patch location, cable count, containment.
- Special items — sensor-activated wardrobe LEDs (
MBR-09,CYB-03,RAF-03), rope/cove
lighting in several rooms, LED bar at the study door, the emergency-light short circuit in the corridor (MCR-04), outdoor waterproof points and the gas line on the front balcony.
What you have. The point counts per room, from data/works_Sheet1.csv (columns: Electrical Points, Light Points, Switch Points, Water Points, Gas Points) and the room-by-room wiring notes in data/lights_plugs_needed_for_Roderick_-ULF.csv. data/rooms.json carries the same information per item, with tags like "Electrical · 4 light points".
Hard constraint. No positions exist for any point. The counts say how many, never where. Any placement is a proposal for Roderick to confirm on site. The existing checklist deliberately labels its point summary "indicative — not a wiring diagram; confirm on site". Keep that discipline: a confidently-wrong electrical drawing on a live site is a safety problem, not a cosmetic one. Malta follows BS 7671-style practice; if you state anything normative, say what you are assuming.
C. Elevations
Per room: switch and socket heights, wardrobe internals, the feature-wall build-up, the vanity (the mirror is specified at 75 cm floor-to-vanity-surface), the corridor mirror (3 m × 1.5 m) and office mirror (2 m × 2 m). The designer's single elevation sheet is images/measurements/furniture_layout_ELEVATIONS_p1.jpg.
D. Smaller gaps
- Back Balcony (
BBL) has one placeholder item and no photo. Scope has to be walked with Roderick. - 54 of 75 site photos are unplaced. Kurt was asked for the order he walked the flat so they
could be assigned by number range — unanswered. See 05-photo-index.md.
- Tiling quantity (OQ-10) is likely wrong and affects both price and the 16-day tiling phase.
- Painting may not be in Roderick's price. If not, Phase 7 is a separate trade and all
subsequent dates shift.
- No materials schedule exists — tile quantities with wastage, adhesive, cable, board, insulation.
Two tile formats are already on site (Fanal 120 and Fanal 31.6×90) and one of them is unexplained.
Sequence worth following
- Base plan from
tiles_sq_m.jpg+ the dimension drawings → flag what needs measuring - Confirm the tiling scope question (OQ-10) against that plan — it may answer itself once areas are computed
- Electrical point layout on the base plan → circuit grouping → board schedule
- Elevations for the rooms with carpentry
- Materials schedule
Each of these can be delivered in the same print-first A4 style as the checklist, and should carry the same honesty markers about what is measured versus assumed.
09-base-plan.md
Base plan — how it was built, and how far to trust it
Deliverable: output/Zonqor Base Plan.pdf (3 pages, A4, print-first). Geometry: build/plan_data.py. Generator: build/build_plan.py.
This closes item A of 08-whats-missing.md. It does not close the need for a survey.
What it is
A dimensioned floor plan of the whole flat — 12 rooms plus two balconies — with a floor area per room, a confidence grade per room, and a list of what has to be measured on site.
It is a reconstruction traced from a designer's drawing, not a measured survey. Nobody has put a tape on this flat. The drawing it comes from carries its own disclaimer: "All dimensions to be verified on site prior to fabrication and installation."
How the scale was established
This is the part that makes the plan usable rather than decorative, so it is worth recording.
images/measurements/kids_bedroom_only_p1.jpg carries a printed metric scale bar. Detecting its tick marks gives:
- major ticks (1 m apart): 60.0 px, every one of them, no drift across the bar
- sub-ticks (0.1 m apart): 6.0 px
- 0 m to 10 m: 599.0 px → 59.90 px/m
images/measurements/study_and_bedroom_revised_p1.jpg is the same drawing plotted larger. Its envelope measures 1.3137× the first sheet, giving 78.68 px/m. That sheet is A4 at 200 dpi, where 78.68 px/m works out to 9.99 mm per metre on paper — exactly 1:100.
Two independent routes to the same scale, agreeing to 0.08%. The second sheet is the base, because it is higher resolution and its callout photos sit clear of the drawing.
How the geometry was extracted
The architectural shell was separated from the furniture by colour: walls are drawn in black (near-neutral, dark), furniture and annotation in blue, red, teal and orange. Masking to max(RGB) < 190 and saturation < 45 leaves the shell alone. Wall lines were then found as projection-profile peaks per band, and the envelope traced as the outermost dark pixel per row. At 200 dpi and 1:100, one pixel is ~13 mm, so single readings are good to roughly ±0.03 m — which is far better than the underlying drawing deserves. The stated ±3% comes from doubting the source drawing, not from the measurement of it.
The flat
A single long bay: 7.21 m wide × 26.00 m deep externally, plus the front balcony beyond the south wall. External walls are 0.22 m thick, consistently on all four sides.
The west side steps in twice. Those two indentations are light wells, and they are the strongest single check on the whole reconstruction: the four rooms that take windows off them — main bathroom, study, ensuite, entrance — are exactly the four rooms the site photos show windows in, and nothing else on that side has one.
A second check: CLAUDE.md §4 records the room order Kurt gave as a physical walk through the flat. Reading the plan from the front door northwards reproduces that order. The reconstruction was not fitted to it — they agree independently.
Only one height is known anywhere in the flat: 2.61 m floor to ceiling in the dining area (830 + 1050 + 730 mm off the feature-wall elevation, openplan_dimensions_1.jpg).
The finding that matters: OQ-10
The 125 m² / 145 m² discrepancy is the two balconies. The two figures were never in conflict — they measure different things.
| m² | |
|---|---|
| Internal rooms, measured off the plan | 133.2 |
| less main bathroom (7.3) and ensuite (3.2) | 122.7 |
designer's figure marked on tiles_sq_m.jpg | 125 |
| back balcony (5.8) + front balcony (~15.8) | 21.6 |
| Roderick's quote | 145 |
122.7 against the designer's 125 is a 1.8% agreement — inside the tolerance of this reconstruction. And 125 + 21.6 = 146.6, against Roderick's 145.
So: 125 m² is the internal floor excluding the wet rooms. 145 m² is everything, balconies included. Both are defensible descriptions of the flat.
That reframes the commercial question. It is not "which number is right" — it is "how much of it is actually being tiled?" The photographs say the ensuite, the main bathroom and the front balcony are already finished. If none of those are re-tiled, the new 120×120 area is nearer 123 m² than 145 — a difference of about 22 m² to settle before a price is agreed.
⚠ This is arithmetic off a reconstructed plan. It does not establish what Roderick intended to include, and it is not a substitute for asking him. It tells you what to ask and roughly how much money is in the answer.
Confidence, per room
Graded on the plan and in the schedule:
- A — read consistently off both sheets. Main bathroom, main corridor, main bedroom, and the
dining/kitchen end of the open plan.
- B — single reading, check on site. Both kids' bedrooms, study, ensuite, living end of the
open plan.
- C — inferred, must be confirmed. Back balcony, small corridor, entrance.
The three weakest points, stated plainly
- The small corridor (
SCR). Geometry puts it between the kids' rooms and the bathroom.
The checklist's room order puts it between the main corridor and the main bedroom. These disagree, and SCR-01 ("extend by 10 cm for the new cabinets") is not specific enough to settle it. Confirm what SCR-01 actually refers to before Phase 4.
- Which kids' bedroom is which.
CYBis "with window",RAFis "with back balcony door".
Both north rooms front the back balcony. The plan cannot tell them apart, so the labels on it are positional (north-west / north-east) and the mapping to Cy and Rafe is unresolved.
- The back balcony. A full-width strip only 0.85 m deep. It is read as a balcony because
tiles_sq_m.jpg excludes it from the shaded area, but it could be a storage recess. BBL already had "scope not established" against it; that is still true.
What must be measured on site
The plan's own last page lists these. In priority order:
- One diagonal per room — nothing here has been checked for square.
- Which kids' room is Cy's and which is Rafe's.
- Where the small corridor actually is.
- The ensuite — internal size, and whether it is being re-tiled at all (drives OQ-05 and OQ-10).
- Floor-to-ceiling height in every room — only the dining area is known.
- Door and window positions and widths — not traced, and needed before any electrical layout.
- The front balcony's curved parapet — chord and depth. A curve cannot be scaled reliably.
Rebuilding it
cd build python3 build_plan.py # writes ../output/base_plan.html python3 pdfplan.py # renders ../output/Zonqor Base Plan.pdf pdftoppm -png -r 110 "../output/Zonqor Base Plan.pdf" /tmp/p # then look at the pages
plan_data.py is the single source of truth for the geometry — room rectangles, the envelope polygon, wall thickness, confidence grades and the measure-on-site list all live there. The generator derives the drawn scale from the viewBox so the "1:130" printed on the sheet cannot drift away from what is actually plotted.
Next: the electrical plan (item B of 08-whats-missing.md) is drawn on top of this base. Do not commit point positions until at least items 1, 5 and 6 above have been measured.
10-project-index.md
index.html — the local project browser
Open index.html in the project root. Double-click it. That is the whole instruction.
It works with no internet, no server, no build step and no installed dependencies. Everything it needs is already on disk beside it.
What it gives you
One page, 35 panes, sidebar navigation with a filter box:
| Section | What is in it |
|---|---|
| Start | Overview — key dates, counts, measured areas, all 12 open questions, and the three things to settle first |
| Deliverables | Site Checklist, Base Plan, Photo Sort Sheet — every page shown as an image, plus a link to the real PDF |
| Documents | CLAUDE.md, README.md and all 10 docs, rendered from markdown |
| Data | items.csv, rooms.json, both works_ sheets, both lights_plugs_ sheets, and the extracted PDF text |
| Images | 15 measurement drawings, 19 designer renders, 75 site photos — captioned, click to enlarge |
| Build scripts | All the Python, so you can read what generated what |
The filter box searches nav items and photo captions, so typing consumer in the site-photos pane narrows to IMG_9845.
Rebuilding it
cd build python3 build_index.py # writes ../index.html
Run it after changing any doc, any data file, or after rebuilding either PDF. It takes a second or two. It regenerates PDF page previews only when the PDF is newer than them.
Three constraints that matter if you move things
index.htmlmust stay in the project root. Images and PDF previews are referenced by
relative path (images/site/…, output/preview/…). Move the file and every image breaks. It is a browser for this folder, not a portable document.
- Text is embedded; images are not. Docs, CSVs, JSON and scripts are pre-rendered into the
HTML by Python. Images and PDF pages are linked. This is deliberate:
- Embedding the 109 images as base64 would make a ~30 MB page.
- Loading the docs at runtime would need
fetch(), andfile://blocksfetch()— the
page would open and silently show nothing. Pre-rendering in Python avoids that entirely.
- PDFs are shown as page images, not in an
<iframe>. An iframe depends on the browser
having a working PDF viewer under file://. That is unreliable — it renders nothing at all in headless Chromium and is inconsistent in Safari. The page images always display. Print from the real PDF, not from the previews — the previews are 96 dpi JPEGs, fine for reading on screen, not for Christine's paper copy.
How the markdown gets rendered
build_index.py carries a ~60-line markdown renderer (headings, tables, fenced code, blockquotes, lists, rules, bold/italic/code/strike/links). That is everything these docs actually use. There is no JavaScript markdown library and no CDN, which is what makes the page work offline. If you add markdown syntax the docs do not currently use, check it renders — the renderer is deliberately minimal, not general.
Photo captions in the galleries are parsed out of docs/05-photo-index.md at build time, so improving that file improves the gallery for free.
It is also hosted
https://zonqor-refurb-c7cc97.pages.dev — Cloudflare Pages, account info@cksloop.com, project zonqor-refurb-c7cc97, first deployed 17 Aug 2026.
Cloudflare Pages serves a directory, which is why the site works unmodified: the relative paths to images/ and output/ resolve exactly as they do locally, at full resolution, with the real PDFs downloadable. Nothing had to be downscaled or embedded.
To redeploy after any change:
./build/deploy.sh # rebuilds index.html, stages, uploads
The script deploys a staged copy, not the project folder. build/, data/, docs/ and _archive/ (the 21 MB handoff zip) are deliberately not uploaded — the docs and data are already embedded inside index.html, and the rest is nobody else's business.
⚠ The hosted copy is public
A *.pages.dev URL is readable by anyone who has it. There is no password on it. The page carries photos of the inside of the flat, the contractor and carpenter names, and the pricing structure. The project name is deliberately unguessable (-c7cc97) so it will not be found by accident, but that is obscurity, not security.
If it needs to be genuinely private, put Cloudflare Access in front of it — Cloudflare dashboard → Zero Trust → Access → Applications → add zonqor-refurb-c7cc97.pages.dev, and allow only your own and Christine's email. That gives a real login. Until then, treat the URL itself as the secret.
Two Cloudflare behaviours worth knowing: it redirects /index.html to /, and it strips .html from paths (/output/Zonqor Site Checklist.html → /output/Zonqor Site Checklist). Both are 308 redirects that browsers follow, so every link on the page still works.
Regenerating everything from scratch
If you ever need to rebuild the whole project offline, in order:
cd build python3 build_v3.py # checklist HTML (stdlib only) python3 verify_refs.py # MUST pass (needs beautifulsoup4) python3 gen_day_plan.py # rewrites docs/04-day-by-day.md python3 pdfcheck.py # checklist PDF (needs playwright + local chromium) cp ../output/renovation_checklist.html "../output/Zonqor Site Checklist.html" python3 build_plan.py # base plan HTML (stdlib only) python3 pdfplan.py # base plan PDF python3 build_index.py # index.html (needs poppler's pdftoppm for page previews)
Everything above runs offline. Python 3.14 is at /opt/homebrew/bin/python3; beautifulsoup4, playwright and its Chromium (~/Library/Caches/ms-playwright/chromium-1208) and poppler (/opt/homebrew/bin/pdftoppm) are all installed locally. Nothing fetches anything.
The only pieces that are not installed are pandas, pdfplumber and pdf2image — and none of them are needed for any of the above. They were one-off tools used to extract data/ from Kurt's original spreadsheets, which is already done.
items.csv
data/items.csv
| id | room_code | room | trade | assigned | phase | title | depends_on | tags | notes |
|---|---|---|---|---|---|---|---|---|---|
| WHS-01 | WHS | Whole House | Gypsum / Structural | Roderick | 1 | Strip out old parquet flooring throughout | start | Gypsum; Remove existing; NEW | Lift and remove the existing parquet across the flat. First job on site — everything else waits on the floor being clear. |
| WHS-02 | WHS | Whole House | Gypsum / Structural | Roderick | 3 | Skip hire and waste removal | WHS-01 — parquet stripped | Gypsum; NEW | At least two skips: one after strip-out/demolition, one after first fix and chasing. Confirm who books and pays for the skip. |
| WHS-03 | WHS | Whole House | Electrical | Roderick | 2 | Fix electricity problems and points | ENT-01 — new consumer unit | Electrical | Diagnose and repair existing faults; check/replace faulty sockets and switches; add points as needed throughout the flat. |
| WHS-04 | WHS | Whole House | Tiling | Roderick | 4 | Check and level floor screed after parquet removal | WHS-01 — parquet stripped | Tiling; NEW | Once the parquet is up, check the screed for level and damage. Patch/level before tiling — 120x120 tiles are unforgiving on an uneven floor. |
| WHS-05 | WHS | Whole House | Tiling | Roderick | 4 | Lay 120x120 tiles — full house, including balconies (~145 sq m) | WHS-04 — floor levelled; first-fix complete | Tiling | 120x120 throughout the flat, ~145 sq m per Roderick's quote. ! Site photos show the ensuite, main bathroom and front balcony are ALREADY TILED — re-measure the actual parquet area before this price is agreed (OQ-10). |
| FBL-01 | FBL | Front Balcony | Electrical | Roderick | 2 | 2 side lights — BBQ side | start | Electrical; 2 light points | |
| FBL-02 | FBL | Front Balcony | Electrical | Roderick | 2 | 1 electrical/USB point — BBQ side (for camera) | start | Electrical; 1 point | |
| FBL-03 | FBL | Front Balcony | Electrical | Roderick | 2 | Central floor lights (under railing) — mini lights or LED strip | start | Electrical; 1 light point | |
| FBL-04 | FBL | Front Balcony | Electrical | Roderick | 2 | Switch for balcony side lights | start | Electrical; 1 switch | At the current kitchen/balcony switch position. |
| FBL-05 | FBL | Front Balcony | Electrical | Roderick | 2 | Switch for balcony floor lights | start | Electrical; 1 switch | At the current kitchen/balcony switch position. |
| FBL-06 | FBL | Front Balcony | Electrical | Roderick | 2 | Rope lighting — front (outdoor) | OQ-02 — outdoor kitchen spec | Electrical | |
| FBL-07 | FBL | Front Balcony | Electrical | Roderick | 2 | 2 outdoor wall lights | OQ-02 — outdoor kitchen spec | Electrical; 2 points | |
| FBL-08 | FBL | Front Balcony | Electrical | Roderick | 2 | 2 waterproof plugs + LED kit for outdoor kitchen units | OQ-02 — outdoor kitchen spec | Electrical | |
| FBL-09 | FBL | Front Balcony | Plumbing / Gas | Roderick | 2 | Run gas line for outdoor kitchen | OQ-02 — outdoor kitchen spec | Gas/Plumbing | Gas run must be pressure-tested and certified. Confirm bottle vs. mains supply and where the bottle housing sits. |
| FBL-10 | FBL | Front Balcony | Fittings / Carpentry | ULF | 9 | Outdoor wall finish — PVC sheet OR tiles | OQ-01 — PVC vs. tiles; elec & gas rough-in (this room) | Fittings/Tiling; ! To confirm | One or the other, not both — still to be decided. See OQ-01. |
| OPN-01 | OPN | Open Plan — Kitchen -> Dining -> Living | Gypsum / Structural | Roderick | 1 | Dining Feature Wall + Fireplace Removal | start | Dining; Gypsum; Remove existing | Remove the existing gypsum feature wall/partition and the fireplace to open the living/dining space. Confirm the final opening size before starting. Check for services in the wall before breaking. |
| OPN-02 | OPN | Open Plan — Kitchen -> Dining -> Living | Gypsum / Structural | Roderick | 5 | Add gypsum behind sofa | OPN-01 — feature wall removed | Living; Gypsum | 250mm wide x 1m high. |
| OPN-03 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | 3 ceiling lights | gypsum (this room) | Kitchen; Electrical; 3 light points | Reuses an existing electrical point. |
| OPN-04 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Hood electrical point | gypsum (this room) | Kitchen; Electrical; 1 point | Reuses an existing electrical point. |
| OPN-05 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Fix oven electrical point — currently trips the salvavita (RCD) | ENT-01 — new consumer unit | Kitchen; Electrical; 2 points; Known issue | Oven trips the salvavita when switched on. Check wiring, the connection point, and whether the oven needs its own dedicated circuit off the new unit. |
| OPN-06 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Light points for dining cabinets | gypsum (this room) | Dining; Electrical; 1 light point | |
| OPN-07 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Rope lighting within the new gypsum wall (behind sofa) | OPN-02 — gypsum behind sofa | Living; Electrical; 1 light point | |
| OPN-08 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Switch for sofa rope lighting | gypsum (this room) | Living; Electrical; 1 switch | |
| OPN-09 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | 2 plugs near sofa | gypsum (this room) | Living; Electrical; 2 points | |
| OPN-10 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | TV unit electrical point | OQ-07 — TV unit electrical spec | Living; Electrical; ! To confirm | |
| OPN-11 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Ethernet point behind sofa | gypsum (this room) | Living; Electrical | Run back to the central unit at the entrance — pull with the first-fix cabling, not after tiling. |
| OPN-12 | OPN | Open Plan — Kitchen -> Dining -> Living | Electrical | Roderick | 2 | Relocate double-pole switch when open-plan wall is removed | OPN-01 — feature wall removed | Dining; Electrical | |
| OPN-13 | OPN | Open Plan — Kitchen -> Dining -> Living | Plumbing / Gas | Roderick | 2 | Re-plumb kitchen sink onto main water feed (remove tank feed) | start | Kitchen; Plumbing; Prep for RO | Switch the sink supply from the tank line to the main feed, and leave it ready for a future reverse-osmosis filter. |
| OPN-14 | OPN | Open Plan — Kitchen -> Dining -> Living | Fittings / Carpentry | ULF | 9 | Install PVC sheet on wall with TV | OQ-06 — PVC sheet spec; tiling complete | Living; Fittings; ! To confirm | |
| ENT-01 | ENT | Entrance | Electrical | Roderick | 2 | Install new consumer unit / RCD (salvavita) panel | OQ-03 — panel location and capacity | Electrical; Major item | Full refresh of the electrical panel. Should also resolve the kitchen oven tripping the salvavita. Confirm final position and capacity (OQ-03) before ordering. Needs certification on completion. |
| ENT-02 | ENT | Entrance | Electrical | Roderick | 2 | Intercom — install / replace unit and wiring | ENT-01 — new consumer unit | Electrical; NEW | Confirm whether the existing intercom is reused or replaced, and whether it is audio-only or video — this changes the cable pulled at first fix. |
| MCR-01 | MCR | Main Corridor | Electrical | Roderick | 2 | Update rope lighting to match new dining rope light | start | Electrical; 1 light point | |
| MCR-02 | MCR | Main Corridor | Electrical | Roderick | 2 | New plug near feature wall | start | Electrical | |
| MCR-03 | MCR | Main Corridor | Electrical | Roderick | 2 | Test existing wire feed | start | Electrical | |
| MCR-04 | MCR | Main Corridor | Electrical | Roderick | 2 | Fix emergency light short circuit | start | Electrical; Known issue | Trace and repair the short before the new unit is energised. |
| MCR-05 | MCR | Main Corridor | Fittings / Carpentry | ULF | 9 | Install vent covers | elec & plumbing rough-in (this room) | Fittings | |
| MCR-06 | MCR | Main Corridor | Fittings / Carpentry | ULF | 9 | Install corridor mirror — 3m x 1.5m | elec & plumbing rough-in (this room) | Fittings | Large and heavy — confirm fixings and who lifts it. Fit after painting. |
| SCR-01 | SCR | Small Corridor | Gypsum / Structural | Roderick | 5 | Extend gypsum partition +10cm to fit new cabinets | start | Gypsum | Existing cabinet LED lighting point to be reused/kept. |
| MBR-01 | MBR | Main Bedroom | Gypsum / Structural | Roderick | 5 | Gypsum — close off old ceiling light area to create new layout | start | Gypsum | |
| MBR-02 | MBR | Main Bedroom | Electrical | Roderick | 1 | Remove 6 existing ceiling lights | start | Electrical; Remove existing | |
| MBR-03 | MBR | Main Bedroom | Electrical | Roderick | 1 | Remove 2 bedside hanging lamps | start | Electrical; Remove existing | |
| MBR-04 | MBR | Main Bedroom | Electrical | Roderick | 1 | Remove bedside window plugs | start | Electrical; Remove existing | |
| MBR-05 | MBR | Main Bedroom | Electrical | Roderick | 2 | 4 new ceiling lights above bed | gypsum (this room) | Electrical; 4 light points | |
| MBR-06 | MBR | Main Bedroom | Electrical | Roderick | 2 | Rope lighting above bed | gypsum (this room) | Electrical; 1 light point | |
| MBR-07 | MBR | Main Bedroom | Electrical | Roderick | 2 | 2 new bedside hanging lamps + 4 switches | gypsum (this room) | Electrical; 2 lights, 4 switches | Switches positioned near bedside. |
| MBR-08 | MBR | Main Bedroom | Electrical | Roderick | 2 | 2 ceiling points near vanity/window | gypsum (this room) | Electrical; 2 light points | |
| MBR-09 | MBR | Main Bedroom | Electrical | Roderick | 2 | Wire for wardrobe LED lighting (sensor-activated) | gypsum (this room) | Electrical; 1 light + sensor | Sensor-operated, not a manual switch. |
| MBR-10 | MBR | Main Bedroom | Electrical | Roderick | 8 | Cut into wardrobe for gyser/pump switches | MBR-11 — wardrobe built (ULF) | Electrical | Gyser and pump switches built into the wardrobe carcass. |
| MBR-11 | MBR | Main Bedroom | Fittings / Carpentry | ULF | 8 | Build wardrobe around ensuite door for maximum storage | tiling and painting complete | Carpentry | |
| MBR-12 | MBR | Main Bedroom | Fittings / Carpentry | ULF | 9 | Install vanity mirror | vanity unit installed (by others) | Fittings; ! To confirm | 75cm from floor to vanity surface — fit after the vanity unit is in. |
| OFF-01 | OFF | Study / Office | Gypsum / Structural | Roderick | 5 | Build new gypsum partition wall | start | Gypsum | 2 layers of board on the inside, 75mm rockwool insulation in the middle, 1 single board on the outer side. |
| OFF-02 | OFF | Study / Office | Electrical | Roderick | 2 | 1 ceiling light point | gypsum (this room) | Electrical; 1 light point | |
| OFF-03 | OFF | Study / Office | Electrical | Roderick | 2 | Wire for LED lighting under desk | gypsum (this room) | Electrical; 1 light point | |
| OFF-04 | OFF | Study / Office | Electrical | Roderick | 2 | Plugs | OQ-04 — plug quantity | Electrical; ! To confirm | |
| OFF-05 | OFF | Study / Office | Electrical | Roderick | 2 | Switch for ceiling light | gypsum (this room) | Electrical; 1 switch | |
| OFF-06 | OFF | Study / Office | Electrical | Roderick | 2 | Switch for under-desk light | gypsum (this room) | Electrical; 1 switch | |
| OFF-07 | OFF | Study / Office | Electrical | Roderick | 2 | Wire LED bar near door | gypsum (this room) | Electrical; 1 light point | |
| OFF-08 | OFF | Study / Office | Electrical | Roderick | 2 | Internet/ethernet point near desk | gypsum (this room) | Electrical | Run back to the central unit at the entrance during first fix. |
| OFF-09 | OFF | Study / Office | Fittings / Carpentry | ULF | 9 | Install office mirror — 2m x 2m | elec & plumbing rough-in (this room) | Fittings | |
| ENS-01 | ENS | Ensuite | Gypsum / Structural | Roderick | 6 | Paint ceiling | tiling complete | Painting | |
| ENS-02 | ENS | Ensuite | Electrical | Roderick | 8 | Add ensuite light switch | MBR-10 — wardrobe cut-out | Electrical; 1 switch | Positioned via the main bedroom wardrobe cut-out. |
| ENS-03 | ENS | Ensuite | Tiling | Roderick | 4 | Tank/waterproof floor and shower walls before tiling | first fix complete | Tiling; NEW | Wet-area waterproofing was not in the original list but must happen before the ensuite is tiled — confirm it is in Roderick's price. |
| ENS-04 | ENS | Ensuite | Tiling | Roderick | 4 | Lay ensuite tiles — 60x30 (dimensions to confirm) | OQ-05 & OQ-10 — confirm scope; ENS-03 waterproofing | Tiling; ! To confirm | ! Site photos show the ensuite is already tiled in large-format travertine. Confirm whether it is being re-tiled at all, or only the shower area (OQ-10). |
| ENS-05 | ENS | Ensuite | Fittings / Carpentry | ULF | 9 | Change shower glass + tray | tiling complete | Fittings | |
| BTH-01 | BTH | Main Bathroom | Plumbing / Gas | Roderick | 2 | Install water heater (gyser) + electrical point | start | Plumbing; 1 point | Confirm gyser size/position before first fix — it sets the pipe and cable runs. |
| BTH-02 | BTH | Main Bathroom | Fittings / Carpentry | ULF | 9 | Replace sink accessories with new | elec & plumbing rough-in (this room) | Fittings | |
| BTH-03 | BTH | Main Bathroom | Fittings / Carpentry | ULF | 9 | Replace flushing accessories with new | elec & plumbing rough-in (this room) | Fittings | |
| BTH-04 | BTH | Main Bathroom | Fittings / Carpentry | ULF | 9 | Attach mirror | elec & plumbing rough-in (this room) | Fittings | |
| BTH-05 | BTH | Main Bathroom | Fittings / Carpentry | ULF | 9 | Attach sink | elec & plumbing rough-in (this room) | Fittings | |
| BTH-06 | BTH | Main Bathroom | Fittings / Carpentry | ULF | 9 | Attach cabinets | elec & plumbing rough-in (this room) | Fittings | |
| CYB-01 | CYB | Cy's Bedroom (with window) | Electrical | Roderick | 2 | Plugs under desk with USB | start | Electrical; 1 point | |
| CYB-02 | CYB | Cy's Bedroom (with window) | Electrical | Roderick | 2 | 2 bedside plugs | start | Electrical; 2 points | |
| CYB-03 | CYB | Cy's Bedroom (with window) | Electrical | Roderick | 2 | Wire for wardrobe LED lighting (sensor-activated) | start | Electrical; 1 light + sensor | |
| CYB-04 | CYB | Cy's Bedroom (with window) | Electrical | Roderick | 2 | Wire feed for LED under wall furniture | start | Electrical | |
| CYB-05 | CYB | Cy's Bedroom (with window) | Electrical | Roderick | 2 | Internet point near desk | start | Electrical | |
| RAF-01 | RAF | Rafe's Bedroom (with back balcony door) | Electrical | Roderick | 2 | Plugs under desk with USB | start | Electrical; 1 point | |
| RAF-02 | RAF | Rafe's Bedroom (with back balcony door) | Electrical | Roderick | 2 | 2 bedside plugs | start | Electrical; 2 points | |
| RAF-03 | RAF | Rafe's Bedroom (with back balcony door) | Electrical | Roderick | 2 | Wire for wardrobe LED lighting (sensor-activated) | start | Electrical; 1 light + sensor | |
| RAF-04 | RAF | Rafe's Bedroom (with back balcony door) | Electrical | Roderick | 2 | Wire feed for LED under wall furniture | start | Electrical | |
| RAF-05 | RAF | Rafe's Bedroom (with back balcony door) | Electrical | Roderick | 2 | Internet point near desk | start | Electrical | |
| BBL-01 | BBL | Back Balcony | Gypsum / Structural | Roderick | 1 | Back balcony — confirm scope on site | start | ! Scope to confirm; NEW | Covered by the whole-house 120x120 tiling (WHS-05). Check for: outdoor lighting, a weatherproof socket, drainage falls, railing condition and door threshold. Add items here as found. |
lights_plugs_needed_for_Roderick_-ULF.csv
data/lights_plugs_needed_for_Roderick_-ULF.csv
| rafe | wall white |
|---|---|
| furniture teal/dark blue | |
| wire feed for light wardrobe with sensor | |
| wire feed for led under wall furtniture | |
| feed for plug on furniture near bed | |
| internet near desk | |
| Cyril | wall teal |
| wire feed for plug on furniture near bed | |
| wire feed for light wardrobe with sensor | |
| wire feed for led under wall furtniture | |
| internet near desk | |
| Back & main corridor | new plug near feature wall |
| to test wire feed | |
| vent covers | |
| Yvette led furniture | |
| chrek emergency light short curcuit with back corridor | |
| main bedroom | Yvette- led around bed |
| cut in wardrobe for switches (gyser/pump) | |
| switch for ensuite? | |
| wire feed for light wardrobe with sensor | |
| wire feed for led around bed | |
| office | internet |
| wire feed for led bar near door | |
| open plan | ethernet for wall behind sofa |
| double pole switch to be moved with wall | |
| unit for salva vita | |
| outdoor | rope lighhtning outdoor for front |
| to pass gas | |
| tiles for wall | |
| outdoor kitchen |
lights_plugs_to_buy.csv
data/lights_plugs_to_buy.csv
| 2 gang | 3 gang | 5gang | 2x plug | 4x plug | plug furniture | wter proof plugs | LED +kit (case/switch) | light sensors- wardrobe | Ceiling lights | lamps | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Rafe | 1 | 3 | 4 | 1 | 1 | |||||||
| Cy | 1 | 2 | 1 | 4 | 1 | 1 | ||||||
| back/main corridor | 3 | 1 | 2 | |||||||||
| main bedroom | 1 | 3 | 2 | 2 | 1 | 8 | 2 hanging lamps | |||||
| Office | 1 | 3 | 1 | 2 | ||||||||
| open plan | 1 | 2 | 1 | 1 | 3 | hood & dining | ||||||
| outdoor | 2 | 1 | 2 wall llights | |||||||||
| 4 | 7 | 3 | 12 | 2 | 10 | 2 | 5 | 3 | 13 | |||
| mirrors | ||||||||||||
| corridor | 3m*1.5m | |||||||||||
| office | 2m*2m | |||||||||||
| main bedroom vanity | to check if ulf and after furniture (75cm from floor to vanity surfice) | |||||||||||
| pvc sheet | outdoor | |||||||||||
| wall with tv | ||||||||||||
| table 2.5m | ||||||||||||
| chairs * 8 |
rooms.json
data/rooms.json · 7 top-level entries
{
"project": "Zonqor Apartment refurbishment",
"generated": "2026-08-16",
"start_date": "2026-08-13",
"work_week": "Mon-Fri",
"crew": "Roderick + son (2), ULF for carpentry",
"rooms": [
{
"code": "WHS",
"name": "Whole House",
"banner": "Cross-room work that is not tied to a single room. Everything here is priced once, for the whole flat.",
"items": [
{
"id": "WHS-01",
"title": "Strip out old parquet flooring throughout",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 1,
"depends_on": "start",
"notes": "Lift and remove the existing parquet across the flat. First job on site \u2014 everything else waits on the floor being clear.",
"tags": [
"Gypsum",
"Remove existing",
"NEW"
]
},
{
"id": "WHS-02",
"title": "Skip hire and waste removal",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 3,
"depends_on": "WHS-01 \u2014 parquet stripped",
"notes": "At least two skips: one after strip-out/demolition, one after first fix and chasing. Confirm who books and pays for the skip.",
"tags": [
"Gypsum",
"NEW"
]
},
{
"id": "WHS-03",
"title": "Fix electricity problems and points",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "ENT-01 \u2014 new consumer unit",
"notes": "Diagnose and repair existing faults; check/replace faulty sockets and switches; add points as needed throughout the flat.",
"tags": [
"Electrical"
]
},
{
"id": "WHS-04",
"title": "Check and level floor screed after parquet removal",
"trade": "Tiling",
"trade_key": "tile",
"assigned": "Roderick",
"phase": 4,
"depends_on": "WHS-01 \u2014 parquet stripped",
"notes": "Once the parquet is up, check the screed for level and damage. Patch/level before tiling \u2014 120x120 tiles are unforgiving on an uneven floor.",
"tags": [
"Tiling",
"NEW"
]
},
{
"id": "WHS-05",
"title": "Lay 120x120 tiles \u2014 full house, including balconies (~145 sq m)",
"trade": "Tiling",
"trade_key": "tile",
"assigned": "Roderick",
"phase": 4,
"depends_on": "WHS-04 \u2014 floor levelled; first-fix complete",
"notes": "120x120 throughout the flat, ~145 sq m per Roderick's quote. ! Site photos show the ensuite, main bathroom and front balcony are ALREADY TILED \u2014 re-measure the actual parquet area before this price is agreed (OQ-10).",
"tags": [
"Tiling"
]
}
],
"pictures": [
{
"file": "tiles_sq_m.jpg",
"caption": "Floor plan \u2014 tiling area reference (125 sqm marked; 145 sqm quoted)"
}
]
},
{
"code": "FBL",
"name": "Front Balcony",
"banner": "! Outdoor kitchen design/equipment still to be specified (OQ-02) before ordering or starting those items.",
"items": [
{
"id": "FBL-01",
"title": "2 side lights \u2014 BBQ side",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"2 light points"
]
},
{
"id": "FBL-02",
"title": "1 electrical/USB point \u2014 BBQ side (for camera)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 point"
]
},
{
"id": "FBL-03",
"title": "Central floor lights (under railing) \u2014 mini lights or LED strip",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 light point"
]
},
{
"id": "FBL-04",
"title": "Switch for balcony side lights",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": "At the current kitchen/balcony switch position.",
"tags": [
"Electrical",
"1 switch"
]
},
{
"id": "FBL-05",
"title": "Switch for balcony floor lights",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": "At the current kitchen/balcony switch position.",
"tags": [
"Electrical",
"1 switch"
]
},
{
"id": "FBL-06",
"title": "Rope lighting \u2014 front (outdoor)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-02 \u2014 outdoor kitchen spec",
"notes": null,
"tags": [
"Electrical"
]
},
{
"id": "FBL-07",
"title": "2 outdoor wall lights",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-02 \u2014 outdoor kitchen spec",
"notes": null,
"tags": [
"Electrical",
"2 points"
]
},
{
"id": "FBL-08",
"title": "2 waterproof plugs + LED kit for outdoor kitchen units",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-02 \u2014 outdoor kitchen spec",
"notes": null,
"tags": [
"Electrical"
]
},
{
"id": "FBL-09",
"title": "Run gas line for outdoor kitchen",
"trade": "Plumbing / Gas",
"trade_key": "plumb",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-02 \u2014 outdoor kitchen spec",
"notes": "Gas run must be pressure-tested and certified. Confirm bottle vs. mains supply and where the bottle housing sits.",
"tags": [
"Gas/Plumbing"
]
},
{
"id": "FBL-10",
"title": "Outdoor wall finish \u2014 PVC sheet OR tiles",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "OQ-01 \u2014 PVC vs. tiles; elec & gas rough-in (this room)",
"notes": "One or the other, not both \u2014 still to be decided. See OQ-01.",
"tags": [
"Fittings/Tiling",
"! To confirm"
]
}
],
"pictures": [
{
"file": "balcony_1.jpg",
"caption": "Front balcony / outdoor kitchen \u2014 render 1"
},
{
"file": "balcony_2.jpg",
"caption": "Front balcony / outdoor kitchen \u2014 render 2"
},
{
"file": "site_9826.jpg",
"caption": "SITE: front balcony \u2014 BBQ corner, existing feed & camera"
},
{
"file": "site_9827.jpg",
"caption": "SITE: front balcony looking out \u2014 adhesive stacked"
},
{
"file": "site_9828.jpg",
"caption": "SITE: front balcony \u2014 already tiled, membrane at edge"
},
{
"file": "site_9895.jpg",
"caption": "SITE: sliding door to front balcony"
}
]
},
{
"code": "OPN",
"name": "Open Plan \u2014 Kitchen -> Dining -> Living",
"banner": "One continuous space. Zone tags below show which end of the room each item sits in.",
"items": [
{
"id": "OPN-01",
"title": "Dining Feature Wall + Fireplace Removal",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 1,
"depends_on": "start",
"notes": "Remove the existing gypsum feature wall/partition and the fireplace to open the living/dining space. Confirm the final opening size before starting. Check for services in the wall before breaking.",
"tags": [
"Dining",
"Gypsum",
"Remove existing"
]
},
{
"id": "OPN-02",
"title": "Add gypsum behind sofa",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 5,
"depends_on": "OPN-01 \u2014 feature wall removed",
"notes": "250mm wide x 1m high.",
"tags": [
"Living",
"Gypsum"
]
},
{
"id": "OPN-03",
"title": "3 ceiling lights",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": "Reuses an existing electrical point.",
"tags": [
"Kitchen",
"Electrical",
"3 light points"
]
},
{
"id": "OPN-04",
"title": "Hood electrical point",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": "Reuses an existing electrical point.",
"tags": [
"Kitchen",
"Electrical",
"1 point"
]
},
{
"id": "OPN-05",
"title": "Fix oven electrical point \u2014 currently trips the salvavita (RCD)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "ENT-01 \u2014 new consumer unit",
"notes": "Oven trips the salvavita when switched on. Check wiring, the connection point, and whether the oven needs its own dedicated circuit off the new unit.",
"tags": [
"Kitchen",
"Electrical",
"2 points",
"Known issue"
]
},
{
"id": "OPN-06",
"title": "Light points for dining cabinets",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Dining",
"Electrical",
"1 light point"
]
},
{
"id": "OPN-07",
"title": "Rope lighting within the new gypsum wall (behind sofa)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OPN-02 \u2014 gypsum behind sofa",
"notes": null,
"tags": [
"Living",
"Electrical",
"1 light point"
]
},
{
"id": "OPN-08",
"title": "Switch for sofa rope lighting",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Living",
"Electrical",
"1 switch"
]
},
{
"id": "OPN-09",
"title": "2 plugs near sofa",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Living",
"Electrical",
"2 points"
]
},
{
"id": "OPN-10",
"title": "TV unit electrical point",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-07 \u2014 TV unit electrical spec",
"notes": null,
"tags": [
"Living",
"Electrical",
"! To confirm"
]
},
{
"id": "OPN-11",
"title": "Ethernet point behind sofa",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": "Run back to the central unit at the entrance \u2014 pull with the first-fix cabling, not after tiling.",
"tags": [
"Living",
"Electrical"
]
},
{
"id": "OPN-12",
"title": "Relocate double-pole switch when open-plan wall is removed",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OPN-01 \u2014 feature wall removed",
"notes": null,
"tags": [
"Dining",
"Electrical"
]
},
{
"id": "OPN-13",
"title": "Re-plumb kitchen sink onto main water feed (remove tank feed)",
"trade": "Plumbing / Gas",
"trade_key": "plumb",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": "Switch the sink supply from the tank line to the main feed, and leave it ready for a future reverse-osmosis filter.",
"tags": [
"Kitchen",
"Plumbing",
"Prep for RO"
]
},
{
"id": "OPN-14",
"title": "Install PVC sheet on wall with TV",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "OQ-06 \u2014 PVC sheet spec; tiling complete",
"notes": null,
"tags": [
"Living",
"Fittings",
"! To confirm"
]
}
],
"pictures": [
{
"file": "openplan_dimensions_1.jpg",
"caption": "Open plan \u2014 measurement drawing 1"
},
{
"file": "openplan_dimensions_2.jpg",
"caption": "Open plan \u2014 measurement drawing 2"
},
{
"file": "openplan_dimensions_3.jpg",
"caption": "Open plan \u2014 measurement drawing 3"
},
{
"file": "1__dining__feature_wall_revised.jpg",
"caption": "Dining \u2014 feature wall render"
},
{
"file": "7__dining_+_kitchen_revised.jpg",
"caption": "Kitchen / dining \u2014 render"
},
{
"file": "15_dining_+_tv_unit_revised.jpg",
"caption": "Dining \u2014 TV unit render"
},
{
"file": "2__sofa_revised.jpg",
"caption": "Living \u2014 sofa wall render"
},
{
"file": "site_9834.jpg",
"caption": "SITE: open plan overview \u2014 parquet still down, 120 tiles delivered"
},
{
"file": "site_9835.jpg",
"caption": "SITE: kitchen island, units wrapped"
},
{
"file": "site_9839.jpg",
"caption": "SITE: dining feature wall + fireplace, wall opened below"
},
{
"file": "site_9893.jpg",
"caption": "SITE: fireplace close-up \u2014 installed, wall broken beneath"
},
{
"file": "site_9840.jpg",
"caption": "SITE: living \u2014 existing socket run"
},
{
"file": "site_9896.jpg",
"caption": "SITE: gypsum board delivered on site"
}
]
},
{
"code": "ENT",
"name": "Entrance",
"banner": "Home of the new electrical central unit \u2014 the first-fix hub for the whole flat. Everything electrical downstream waits on ENT-01.",
"items": [
{
"id": "ENT-01",
"title": "Install new consumer unit / RCD (salvavita) panel",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-03 \u2014 panel location and capacity",
"notes": "Full refresh of the electrical panel. Should also resolve the kitchen oven tripping the salvavita. Confirm final position and capacity (OQ-03) before ordering. Needs certification on completion.",
"tags": [
"Electrical",
"Major item"
]
},
{
"id": "ENT-02",
"title": "Intercom \u2014 install / replace unit and wiring",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "ENT-01 \u2014 new consumer unit",
"notes": "Confirm whether the existing intercom is reused or replaced, and whether it is audio-only or video \u2014 this changes the cable pulled at first fix.",
"tags": [
"Electrical",
"NEW"
]
}
],
"pictures": [
{
"file": "site_9845.jpg",
"caption": "SITE: existing consumer unit, thermostat & entrance door"
}
]
},
{
"code": "MCR",
"name": "Main Corridor",
"banner": null,
"items": [
{
"id": "MCR-01",
"title": "Update rope lighting to match new dining rope light",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 light point"
]
},
{
"id": "MCR-02",
"title": "New plug near feature wall",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical"
]
},
{
"id": "MCR-03",
"title": "Test existing wire feed",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical"
]
},
{
"id": "MCR-04",
"title": "Fix emergency light short circuit",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": "Trace and repair the short before the new unit is energised.",
"tags": [
"Electrical",
"Known issue"
]
},
{
"id": "MCR-05",
"title": "Install vent covers",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
},
{
"id": "MCR-06",
"title": "Install corridor mirror \u2014 3m x 1.5m",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": "Large and heavy \u2014 confirm fixings and who lifts it. Fit after painting.",
"tags": [
"Fittings"
]
}
],
"pictures": [
{
"file": "10_corridor_revised.jpg",
"caption": "Main corridor \u2014 render"
},
{
"file": "corridor_dimensions_1.jpg",
"caption": "Corridor \u2014 measurement drawing"
},
{
"file": "site_9849.jpg",
"caption": "SITE: main corridor looking through"
},
{
"file": "site_9850.jpg",
"caption": "SITE: main corridor \u2014 existing cove lighting"
},
{
"file": "site_9843.jpg",
"caption": "SITE: corridor switch position"
}
]
},
{
"code": "SCR",
"name": "Small Corridor",
"banner": null,
"items": [
{
"id": "SCR-01",
"title": "Extend gypsum partition +10cm to fit new cabinets",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 5,
"depends_on": "start",
"notes": "Existing cabinet LED lighting point to be reused/kept.",
"tags": [
"Gypsum"
]
}
],
"pictures": [
{
"file": "12_corridor_small_revised.jpg",
"caption": "Small corridor \u2014 render"
}
]
},
{
"code": "MBR",
"name": "Main Bedroom",
"banner": null,
"items": [
{
"id": "MBR-01",
"title": "Gypsum \u2014 close off old ceiling light area to create new layout",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 5,
"depends_on": "start",
"notes": null,
"tags": [
"Gypsum"
]
},
{
"id": "MBR-02",
"title": "Remove 6 existing ceiling lights",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 1,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"Remove existing"
]
},
{
"id": "MBR-03",
"title": "Remove 2 bedside hanging lamps",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 1,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"Remove existing"
]
},
{
"id": "MBR-04",
"title": "Remove bedside window plugs",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 1,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"Remove existing"
]
},
{
"id": "MBR-05",
"title": "4 new ceiling lights above bed",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"4 light points"
]
},
{
"id": "MBR-06",
"title": "Rope lighting above bed",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"1 light point"
]
},
{
"id": "MBR-07",
"title": "2 new bedside hanging lamps + 4 switches",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": "Switches positioned near bedside.",
"tags": [
"Electrical",
"2 lights, 4 switches"
]
},
{
"id": "MBR-08",
"title": "2 ceiling points near vanity/window",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"2 light points"
]
},
{
"id": "MBR-09",
"title": "Wire for wardrobe LED lighting (sensor-activated)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": "Sensor-operated, not a manual switch.",
"tags": [
"Electrical",
"1 light + sensor"
]
},
{
"id": "MBR-10",
"title": "Cut into wardrobe for gyser/pump switches",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 8,
"depends_on": "MBR-11 \u2014 wardrobe built (ULF)",
"notes": "Gyser and pump switches built into the wardrobe carcass.",
"tags": [
"Electrical"
]
},
{
"id": "MBR-11",
"title": "Build wardrobe around ensuite door for maximum storage",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 8,
"depends_on": "tiling and painting complete",
"notes": null,
"tags": [
"Carpentry"
]
},
{
"id": "MBR-12",
"title": "Install vanity mirror",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "vanity unit installed (by others)",
"notes": "75cm from floor to vanity surface \u2014 fit after the vanity unit is in.",
"tags": [
"Fittings",
"! To confirm"
]
}
],
"pictures": [
{
"file": "main_bedroom_1.jpg",
"caption": "Main bedroom \u2014 render (overview)"
},
{
"file": "main_bedroom_6-bed.jpg",
"caption": "Main bedroom \u2014 bed wall render"
},
{
"file": "main_bedroom_4-_wardrobe.jpg",
"caption": "Main bedroom \u2014 wardrobe render"
},
{
"file": "main_bedroom_5-_vanity.jpg",
"caption": "Main bedroom \u2014 vanity render"
},
{
"file": "main_bedroom_dimensions_1.jpg",
"caption": "Main bedroom \u2014 measurement drawing 1"
},
{
"file": "main_bedroom_dimensions_2.jpg",
"caption": "Main bedroom \u2014 measurement drawing 2"
}
]
},
{
"code": "OFF",
"name": "Study / Office",
"banner": "Sits off the Main Bedroom. ! The new partition is a full wall, not an amendment \u2014 build it BEFORE tiling, not after (see the sequencing note in the plan).",
"items": [
{
"id": "OFF-01",
"title": "Build new gypsum partition wall",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 5,
"depends_on": "start",
"notes": "2 layers of board on the inside, 75mm rockwool insulation in the middle, 1 single board on the outer side.",
"tags": [
"Gypsum"
]
},
{
"id": "OFF-02",
"title": "1 ceiling light point",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"1 light point"
]
},
{
"id": "OFF-03",
"title": "Wire for LED lighting under desk",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"1 light point"
]
},
{
"id": "OFF-04",
"title": "Plugs",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "OQ-04 \u2014 plug quantity",
"notes": null,
"tags": [
"Electrical",
"! To confirm"
]
},
{
"id": "OFF-05",
"title": "Switch for ceiling light",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"1 switch"
]
},
{
"id": "OFF-06",
"title": "Switch for under-desk light",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"1 switch"
]
},
{
"id": "OFF-07",
"title": "Wire LED bar near door",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": null,
"tags": [
"Electrical",
"1 light point"
]
},
{
"id": "OFF-08",
"title": "Internet/ethernet point near desk",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "gypsum (this room)",
"notes": "Run back to the central unit at the entrance during first fix.",
"tags": [
"Electrical"
]
},
{
"id": "OFF-09",
"title": "Install office mirror \u2014 2m x 2m",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
}
],
"pictures": [
{
"file": "study_1.jpg",
"caption": "Study / office \u2014 render 1"
},
{
"file": "study_2.jpg",
"caption": "Study / office \u2014 render 2"
},
{
"file": "study_3.jpg",
"caption": "Study / office \u2014 render 3"
}
]
},
{
"code": "ENS",
"name": "Ensuite",
"banner": null,
"items": [
{
"id": "ENS-01",
"title": "Paint ceiling",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 6,
"depends_on": "tiling complete",
"notes": null,
"tags": [
"Painting"
]
},
{
"id": "ENS-02",
"title": "Add ensuite light switch",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 8,
"depends_on": "MBR-10 \u2014 wardrobe cut-out",
"notes": "Positioned via the main bedroom wardrobe cut-out.",
"tags": [
"Electrical",
"1 switch"
]
},
{
"id": "ENS-03",
"title": "Tank/waterproof floor and shower walls before tiling",
"trade": "Tiling",
"trade_key": "tile",
"assigned": "Roderick",
"phase": 4,
"depends_on": "first fix complete",
"notes": "Wet-area waterproofing was not in the original list but must happen before the ensuite is tiled \u2014 confirm it is in Roderick's price.",
"tags": [
"Tiling",
"NEW"
]
},
{
"id": "ENS-04",
"title": "Lay ensuite tiles \u2014 60x30 (dimensions to confirm)",
"trade": "Tiling",
"trade_key": "tile",
"assigned": "Roderick",
"phase": 4,
"depends_on": "OQ-05 & OQ-10 \u2014 confirm scope; ENS-03 waterproofing",
"notes": "! Site photos show the ensuite is already tiled in large-format travertine. Confirm whether it is being re-tiled at all, or only the shower area (OQ-10).",
"tags": [
"Tiling",
"! To confirm"
]
},
{
"id": "ENS-05",
"title": "Change shower glass + tray",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "tiling complete",
"notes": null,
"tags": [
"Fittings"
]
}
],
"pictures": [
{
"file": "site_9886.jpg",
"caption": "SITE: ensuite \u2014 existing shower enclosure"
},
{
"file": "site_9888.jpg",
"caption": "SITE: ensuite \u2014 shower tray & glass to be changed"
},
{
"file": "site_9889.jpg",
"caption": "SITE: ensuite \u2014 WC & window"
},
{
"file": "site_9890.jpg",
"caption": "SITE: ensuite \u2014 overview"
}
]
},
{
"code": "BTH",
"name": "Main Bathroom",
"banner": null,
"items": [
{
"id": "BTH-01",
"title": "Install water heater (gyser) + electrical point",
"trade": "Plumbing / Gas",
"trade_key": "plumb",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": "Confirm gyser size/position before first fix \u2014 it sets the pipe and cable runs.",
"tags": [
"Plumbing",
"1 point"
]
},
{
"id": "BTH-02",
"title": "Replace sink accessories with new",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
},
{
"id": "BTH-03",
"title": "Replace flushing accessories with new",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
},
{
"id": "BTH-04",
"title": "Attach mirror",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
},
{
"id": "BTH-05",
"title": "Attach sink",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
},
{
"id": "BTH-06",
"title": "Attach cabinets",
"trade": "Fittings / Carpentry",
"trade_key": "fit",
"assigned": "ULF",
"phase": 9,
"depends_on": "elec & plumbing rough-in (this room)",
"notes": null,
"tags": [
"Fittings"
]
}
],
"pictures": [
{
"file": "site_9872.jpg",
"caption": "SITE: main bathroom \u2014 existing finish"
},
{
"file": "site_9873.jpg",
"caption": "SITE: main bathroom \u2014 WC & window"
},
{
"file": "site_9875.jpg",
"caption": "SITE: main bathroom \u2014 ceiling & downlights"
}
]
},
{
"code": "CYB",
"name": "Cy's Bedroom (with window)",
"banner": null,
"items": [
{
"id": "CYB-01",
"title": "Plugs under desk with USB",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 point"
]
},
{
"id": "CYB-02",
"title": "2 bedside plugs",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"2 points"
]
},
{
"id": "CYB-03",
"title": "Wire for wardrobe LED lighting (sensor-activated)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 light + sensor"
]
},
{
"id": "CYB-04",
"title": "Wire feed for LED under wall furniture",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical"
]
},
{
"id": "CYB-05",
"title": "Internet point near desk",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical"
]
}
],
"pictures": [
{
"file": "Cy_kids_bedroom_7.jpg",
"caption": "Cy's bedroom \u2014 render 1"
},
{
"file": "Cy_kids_bedroom_10.jpg",
"caption": "Cy's bedroom \u2014 render 2"
},
{
"file": "kids_bedroom_dimensions_2.jpg",
"caption": "Kids bedroom \u2014 measurement drawing"
}
]
},
{
"code": "RAF",
"name": "Rafe's Bedroom (with back balcony door)",
"banner": null,
"items": [
{
"id": "RAF-01",
"title": "Plugs under desk with USB",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 point"
]
},
{
"id": "RAF-02",
"title": "2 bedside plugs",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"2 points"
]
},
{
"id": "RAF-03",
"title": "Wire for wardrobe LED lighting (sensor-activated)",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical",
"1 light + sensor"
]
},
{
"id": "RAF-04",
"title": "Wire feed for LED under wall furniture",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical"
]
},
{
"id": "RAF-05",
"title": "Internet point near desk",
"trade": "Electrical",
"trade_key": "elec",
"assigned": "Roderick",
"phase": 2,
"depends_on": "start",
"notes": null,
"tags": [
"Electrical"
]
}
],
"pictures": [
{
"file": "Rafe_kids_bedroom_1.jpg",
"caption": "Rafe's bedroom \u2014 render 1"
},
{
"file": "Rafe_kids_bedroom_5.jpg",
"caption": "Rafe's bedroom \u2014 render 2"
},
{
"file": "kids_bedroom_dimensions_1.jpg",
"caption": "Kids bedroom \u2014 measurement drawing"
}
]
},
{
"code": "BBL",
"name": "Back Balcony",
"banner": "Off Rafe's bedroom. No items listed yet beyond the whole-house tiling \u2014 walk it with Roderick on day one and write in anything found.",
"items": [
{
"id": "BBL-01",
"title": "Back balcony \u2014 confirm scope on site",
"trade": "Gypsum / Structural",
"trade_key": "gypsum",
"assigned": "Roderick",
"phase": 1,
"depends_on": "start",
"notes": "Covered by the whole-house 120x120 tiling (WHS-05). Check for: outdoor lighting, a weatherproof socket, drainage falls, railing condition and door threshold. Add items here as found.",
"tags": [
"! Scope to confirm",
"NEW"
]
}
],
"pictures": []
}
],
"phases": {
"1": {
"name": "Strip-out & demolition",
"description": "Parquet up, feature wall and fireplace down, old fittings removed."
},
"2": {
"name": "First fix \u2014 plumbing, electrical, ethernet",
"description": "Consumer unit, chasing, all cable and pipe runs, gas line. Nothing gets closed up until this is signed off."
},
"3": {
"name": "Clear site \u2014 skip",
"description": "Rubble and chasing debris out before tiling starts."
},
"4": {
"name": "Tiling",
"description": "Screed levelled, wet areas tanked, 120x120 laid through the flat and balconies, ensuite in 60x30."
},
"5": {
"name": "Gypsum \u2014 partition & amendments",
"description": "Study partition, main bedroom ceiling closure, sofa wall, small corridor extension."
},
"6": {
"name": "Painting",
"description": "Ceilings and walls, including the ensuite ceiling."
},
"7": {
"name": "Second fix electrical",
"description": "Fit and terminate every point, hang light fittings, LED strips and sensors, then test and certify."
},
"8": {
"name": "Carpentry \u2014 ULF",
"description": "Wardrobes, cabinets, TV unit. Roderick returns to cut in the gyser/pump and ensuite switches once the wardrobe is up."
},
"9": {
"name": "Fittings, sanitary & outdoor",
"description": "Mirrors, shower glass and tray, sanitary ware, vent covers, PVC/tile outdoor wall finish."
},
"10": {
"name": "Snagging & handover",
"description": "Walk the flat against this checklist, list defects, fix, final clean, hand over certificates."
}
}
}works_Sheet1.csv
data/works_Sheet1.csv
| NEED NEW | 24 | 14 | 44 | 44 | 18 | 0 | 0 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| # | NEW / EXIST | ACTION | ROOM | LOCATION | DESCRIPTION | FOR | Gypsum/paint works | Furniture Setup | Electrical Points | Light Points | Switch Points | Water Points | Gas Points | IMAGE | |||||
| Exist | Keep | Balcony | BBQ Side | 2 WaterProof Electrical Points | General Use | ç | |||||||||||||
| Exist | Keep | Balcony | BBQ Side | Water Tap | General Use | ||||||||||||||
| Exist | Keep | Balcony | Ceiling | Main Ceiling Light | Top Lights | ||||||||||||||
| Exist | Keep | Balcony | Gas Points | 2 Gas Points | Fire Place / Cooker | ||||||||||||||
| 1 | New | ToDo | Balcony | BBQ Side | 2 Lights Points | Side Lights | 2 | ||||||||||||
| 2 | New | ToDo | Balcony | BBQ Side | 1 Electrical / USB Point | Camera | 1 | ||||||||||||
| 3 | New | ToDo | Balcony | Central Floor (Under Railing) | Central Floor (Under Railing) - Mini Lights OR LED | Floor Lights | 1 | ||||||||||||
| 4 | New | ToDo | Balcony | Kitchen - Current Balcany Switch | Switch for Side Lights | 1 | |||||||||||||
| 5 | New | ToDo | Balcony | Kitchen - Current Balcany Switch | Switch for Floor Lights | 1 | |||||||||||||
| Exist | Remove | Main Bedroom | Ceiling | 6 ceiling lights | 6 | ||||||||||||||
| Exist | Remove | Main Bedroom | Hanging Lamps | 2 bed side hanging lamps | 2 | ||||||||||||||
| Exist | Remove | Main Bedroom | Bed Side Window | bed side plugs | 1 | ||||||||||||||
| Exist | Keep | Main Bedroom | Bed Side Wall | bed side plugs | |||||||||||||||
| Exist | Keep | Main Bedroom | AC | 1 Ac point _ double pole switch | |||||||||||||||
| Exist | Keep | Main Bedroom | Tv Unit | Double Point | |||||||||||||||
| Exist | Keep | Main Bedroom | Vanity Unit | Double Point | |||||||||||||||
| Exist | Keep | Main Bedroom | Behind Door | Double Point | |||||||||||||||
| New | ToDo | Main Bedroom | Ceiling | 4 ceiling lights above bed | 4 | ||||||||||||||
| New | ToDo | Main Bedroom | Bed | Rope lightning above bed | 1 | ||||||||||||||
| New | ToDo | Main Bedroom | Side bed hanging lamps | 2 bedside hanging lamps | 2 | ||||||||||||||
| New | ToDo | Main Bedroom | Side bed Switches | Switch for above- near bedside | 4 | ||||||||||||||
| New | ToDo | Main Bedroom | Vanity / Window ceiling | 2 ceiling points | 2 | ||||||||||||||
| New | ToDo | Main Bedroom | Wardrobe | LED Light Point | 1 | ||||||||||||||
| New | ToDo | Main Bedroom | Gypsum | Close of light ceiling to create new | 6 | ||||||||||||||
| New | To do | Study | Gypsum | inside with 2 layers boards- Inside with 75mm rockwool insulation- outerpart 1 single board | 2 | ||||||||||||||
| New | To do | Study | Ceiling | 1 light point | 1 | ||||||||||||||
| New | To do | Study | light under desk | 1 light point | 1 | ||||||||||||||
| New | To do | Study | plugs | ? | ? | ||||||||||||||
| New | To do | Study | Switch | For ceiling | 1 | ||||||||||||||
| New | To do | Study | Switch | under desk | 1 | ||||||||||||||
| Exist | Keep | Rafe Bedroom | current switch (room ceiling+balcony) | ||||||||||||||||
| Exist | Keep | Rafe Bedroom | AC | 1 Ac point _ double pole switch | |||||||||||||||
| Exist | Keep | Rafe Bedroom | Plugs behind wardrobe | ||||||||||||||||
| Exist | Keep | Rafe Bedroom | Plugs near ac /desk | ||||||||||||||||
| New | To do | Rafe Bedroom | Plugs under desk with USB | 1 | |||||||||||||||
| New | To do | Rafe Bedroom | plug bedside | 2 | |||||||||||||||
| Exist | Keep | Cy Bedroom | current switch (room ceiling) | ||||||||||||||||
| Exist | Keep | Cy Bedroom | AC | 1 Ac point _ double pole switch | |||||||||||||||
| Exist | Keep | Cy Bedroom | Plugs near wardrobe + other walls | ||||||||||||||||
| Exist | Keep | Cy Bedroom | Plugs near ac /desk | ||||||||||||||||
| New | To do | Cy Bedroom | Plugs under desk with USB | 1 | |||||||||||||||
| New | To do | Cy Bedroom | plug bedside | 2 | |||||||||||||||
| Exist | Keep | Dining | wall rope lightning | to fix | 1 | ||||||||||||||
| Exist | Keep | Dining | ceiling lights | ||||||||||||||||
| Exist | Keep | Dining | plug near balcony | christmas tree | |||||||||||||||
| New | To do | Dining | light points for cabinets | 1 | |||||||||||||||
| New | To do | Dining | removal of gypsum feature wall | 1 | |||||||||||||||
| Exist | Keep | Sofa | ceiling lights | ||||||||||||||||
| New | To do | Sofa | gypsum | add gyspum behind sofa 250mm wide- 1m high | 1 | ||||||||||||||
| New | To do | Sofa | rope lightning | within the gypsum wall | 1 | ||||||||||||||
| New | To do | Sofa | switch | to switch rope lightning | 1 | ||||||||||||||
| New | To do | Sofa | plug | 2 | |||||||||||||||
| Sofa | tv unit | ? | |||||||||||||||||
| New | To do | Kitchen | Ceiling lights | existing electrial point | 3 | ||||||||||||||
| New | To do | Kitchen | Hood | existing electrial point | 1 | ||||||||||||||
| New | To do | Kitchen | Plumbing | Main water with kitchen | |||||||||||||||
| Exist | To do | Kitchen | Oven point | To fix oven plug electrical point | 2 | ||||||||||||||
| Exist | Keep | Long Corridor | Ceiling Lights | ||||||||||||||||
| Exist | Keep | Long Corridor | wall feature lights | ||||||||||||||||
| Exist | Keep | Long Corridor | Rope lightning | to change to match new dining rope | 1 | ||||||||||||||
| Exist | Keep | Small Corridor | Ceiling Lights | ||||||||||||||||
| Exist | Keep | Small Corridor | Electrical point | Cabinet lightnings LED | |||||||||||||||
| New | To do | Small Corridor | Gypsum | add extra 10 cm with current partion to add cabinets | 1 | ||||||||||||||
| Exist | Keep | Ensuite | Ceiling lights | ||||||||||||||||
| New | To do | Ensuite | Change shower glass + tray | 1 | |||||||||||||||
| New | To do | Ensuite | Ceiling | need painting | 1 | ||||||||||||||
| Exist | To do | Main Bathroom | accssories-sink | replace with new | 1 | ||||||||||||||
| Exist | To do | Main Bathroom | accssories-flushing | replace with new | 1 | ||||||||||||||
| New | To do | Main Bathroom | Mirror | to attach | 1 | ||||||||||||||
| New | To do | Main Bathroom | sink | to attach | 1 | ||||||||||||||
| New | To do | Main Bathroom | cabinets | to attach | 1 | ||||||||||||||
| New | To do | Main Bathroom | Gyser | 1 | 1 | ||||||||||||||
| 12 | 7 | 22 | 22 | 9 | |||||||||||||||
| roderick28@hotmail.co.uk | |||||||||||||||||||
| Fittings | Bathrooms | ||||||||||||||||||
| Madum |
works_Sheet2.csv
data/works_Sheet2.csv
| Roderick | Euros Per sqm | 145msq | |
|---|---|---|---|
| tiles | 120x20 | 30 | 4500 |
| skirting | 4.5 | 675 | |
| paint | 5.5 | ||
| Floor balcony | 40 | 400 | |
| Bedroom office | 1100 | ||
| behind sofa | 350 | ||
| Point electric | 35 | 1855 | |
| Back balcony tiles (same as front) | |||
| change water from sink | |||
| gyser | |||
| bathroom cabinets | |||
| bathroom ensuite to change | |||
| small corridor partition |
pdf_text/ — extracted drawing text
Copy_of__furniture_+_gypsum_measurement.txt
SOURCE: Furniture measurements/Copy of _furniture + gypsum measurement.pdf PAGES: 1 --- page 1 --- 0052 low cabinets high cabinets for storage wardrobe around ensuite door for maximum storage Entryway unit 1775 250 054 800 500 500 rorrim Sound proof gypsum partition to consist of' 1. Inside of study to have 2 layers of boards 2. Middle part to have a profile of 75 mm filled with rockwool insulation 3. Outer part of study to have a single board 1952 0011 600 ksed dna deb evoba stenibac 0012 sevlehs desk side storage and shelving above 1400 0002 350 300 005 350 0 0001 0002 side storage cabinets above bed tv unit shelving enclosed storage 1563,97 0002 drawers large mirror with dressing table 058 700 054 1050 500 5982 0981 500 0321 dap gniklaw 600 610 700 900 tnempiuqe myg 1500 standing desk 006 311 113 1600 0972 600
Copy_of_kids_bedroom_measurement_opt4.txt
SOURCE: Furniture measurements/Copy of kids bedroom measurement_opt4.pdf PAGES: 1 --- page 1 --- low cabinets high cabinets for storage 350 2591 1100 600 cabinets above bed and desk 2100 shelves desk evoba gnivlehs dna egarots edis 1400 2000 350 300 500 500 350 1000 2000 cabinets above bed side storage O p t i o n 4
balcony_plan.txt
SOURCE: Furniture measurements/balcony plan.pdf PAGES: 1 --- page 1 --- egarots deso gas cylinders under bbq 800 600 1500 900
furniture_layout_ELEVATIONS.txt
SOURCE: Furniture measurements/Elevations/furniture layout ELEVATIONS.pdf PAGES: 1 --- page 1 --- 0052 low cabinets high cabinets for storage wardrobe around ensuite door for maximum storage Entryway unit 1775 250 054 800 500 500 rorrim Sound proof gypsum partition to consist of' 1. Inside of study to have 2 layers of boards 2. Middle part to have a profile of 75 mm filled with rockwool insulation 3. Outer part of study to have a single board 1952 0011 600 ksed dna deb evoba stenibac 0002 sevlehs desk side storage and shelving above 1500 0002 350 200 005 350 0001 0002 side storage cabinets above bed 0002 tv unit Open shelving drawers large mirror with dressing table 058 700 054 1150 500 5982 500 0321 dap gniklaw 610 600 600 800 tnempiuqe myg 1500 standing desk 006 311 113 1600 0972 600 052 0002 1050 500 500 675 1500 1400 1400 350 1600 1200 gas cylinders under bbq 008 1500 600 005 005 009 036 005 005 950 052052052 008 004 008 0531 008 004 008 Open shelving 380 0984
kids_bedroom_only.txt
SOURCE: Furniture measurements/kids bedroom only.pdf PAGES: 1 --- page 1 --- 0462 0052 Entryway unit 1775 004 1400 1400 0002 elevated furniture drawers desk sevlehs 1400 0002 ladder ksed reddal drawers flehs koob 250 600 005 0012 1900 0091 039 006 300 600 0052 006 500 0041 1200 059 low cabinets high cabinets for storage 053 0011 bunk beds with desk underneath 1600 Option 1 1 0 1 2 3 4 5 6 7 8 9 10 m 0 10 20 30 ft scale 1:100 ksed 0002 870 600 5532 drawers lwairtghe dmreirsrsoirng table shelving ctaalblinets 001 058 1570 700 054 1050 500 5982 0981 500 700 850 wardrobe around ensuite door for maximum storage Gypsum wall
study_and_bedroom_revised.txt
SOURCE: Furniture measurements/study and bedroom revised.pdf PAGES: 1 --- page 1 --- 0052 low cabinets high cabinets for storage wardrobe around ensuite door for maximum storage Entryway unit 1775 250 054 800 500 500 rorrim Sound proof gypsum partition to consist of' 1. Inside of study to have 2 layers of boards 2. Middle part to have a profile of 75 mm filled with rockwool insulation 3. Outer part of study to have a single board 1952 0011 600 ksed dna deb evoba stenibac 0012 sevlehs desk side storage and shelving above 1400 0002 350 300 005 350 0 0001 0002 side storage cabinets above bed tv unit shelving enclosed storage 1563,97 0002 drawers large mirror with dressing table 058 700 054 1050 500 5982 0981 500 0321 dap gniklaw 600 610 700 900 tnempiuqe myg 1500 standing desk 006 311 113 1600 0972 600
Measurement drawings
15 images. Click any image to enlarge. These are the dimensional source of truth. Two of them carry the scale bar the base plan was calibrated on.

—

—

—

—

—

—

—

—

—

—

—

—

—

—

—
Designer renders
19 images. Click any image to enlarge. Visual intent only — no dimensions.

—

—

—

—

—

—

—

—

—

—

—

—

—

—

—

—

—

—

—
Site photos (August 2026)
75 images. Click any image to enlarge. 21 are placed in the checklist, 54 are not. Captions come from docs/05-photo-index.md.

front balcony — BBQ corner, existing feed & camera

front balcony looking out, adhesive stacked

front balcony — already tiled, membrane at edge

balcony exterior wall, sea view

—

—

feature wall niche/alcove

—

open plan overview — parquet still down, Fanal 120 tiles delivered

kitchen island, units wrapped

corner, skirting, parquet

wall switch plate

ceiling downlights + kitchen units

dining feature wall + fireplace, wall opened below

living — existing socket run

built-in bench with socket

bench/shelf run

corridor switch position

corridor/entry area with hooks

EXISTING CONSUMER UNIT + thermostat + entrance door — key for OQ-03

—

—

—

main corridor looking through

main corridor — existing cove lighting

—

—

—

—

—

—

—

—

—

—

—

—

—

corridor ceiling downlight

—

—

—

—

—

—

—

main bathroom — existing grey marble finish

main bathroom — WC & window

grey marble bathroom, bedding stored in front

main bathroom — ceiling & downlights

—

—

—

—

—

—

—

—

—

ensuite WC + flush plate

ensuite — existing shower enclosure

ensuite ceiling/shower from above

ensuite — shower tray & glass (to be changed, ENS-05)

ensuite — WC & window

ensuite — overview

doorway, taped

recessed hatch with AV/equipment inside

fireplace close-up — installed, wall broken beneath

Fanal 31.6x90 tile boxes stacked on balcony — source of OQ-12

sliding door to front balcony

gypsum board delivered on site

—

—

—

—
build_index.py
build/build_index.py
"""Build index.html — a local browser for everything in this project.
python3 build_index.py # writes ../index.html, then open it in a browser
Design notes:
* Text files (docs, CSV, JSON, scripts) are EMBEDDED, pre-rendered here in Python. That keeps
the page working offline with no JS libraries and no fetch() — file:// blocks fetch, so an
index that loaded its own docs at runtime would silently show nothing.
* Images and PDFs are REFERENCED by relative path. img/iframe src is not blocked under file://,
and embedding 109 images as base64 would make a ~30 MB page.
* Therefore index.html must stay at the project root, beside images/ and output/.
"""
import os, re, json, html, csv, io, datetime
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.normpath(os.path.join(HERE, ".."))
rel = lambda *p: os.path.join(ROOT, *p)
read = lambda p: open(p, encoding="utf-8", errors="replace").read()
E = html.escape
# --------------------------------------------------------------------- markdown -> html
def md(src):
"""Small line-based markdown renderer. Covers what the docs actually use:
headings, tables, fenced code, blockquotes, lists, hr, bold/italic/code/strike, links."""
out, lines, i = [], src.split("\n"), 0
list_stack = []
def inline(t):
t = E(t)
t = re.sub(r"`([^`]+)`", r"<code>\1</code>", t)
t = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", t)
t = re.sub(r"~~([^~]+)~~", r"<del>\1</del>", t)
t = re.sub(r"(?<![*\w])\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", t)
t = re.sub(r"\[\[([^\]]+)\]\]", r"<em>\1</em>", t)
t = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', t)
return t
def close_lists():
while list_stack:
out.append(f"</{list_stack.pop()}>")
while i < len(lines):
ln = lines[i]
if ln.startswith("```"):
close_lists(); i += 1; buf = []
while i < len(lines) and not lines[i].startswith("```"):
buf.append(lines[i]); i += 1
out.append("<pre class='code'>" + E("\n".join(buf)) + "</pre>"); i += 1; continue
if re.match(r"^\s*\|", ln) and i+1 < len(lines) and re.match(r"^\s*\|[\s:|-]+\|?\s*$", lines[i+1]):
close_lists()
cells = lambda r: [c.strip() for c in r.strip().strip("|").split("|")]
head = cells(ln); i += 2; body = []
while i < len(lines) and re.match(r"^\s*\|", lines[i]):
body.append(cells(lines[i])); i += 1
t = ["<table><thead><tr>" + "".join(f"<th>{inline(c)}</th>" for c in head) + "</tr></thead><tbody>"]
for r in body:
t.append("<tr>" + "".join(f"<td>{inline(c)}</td>" for c in r) + "</tr>")
out.append("".join(t) + "</tbody></table>"); continue
if re.match(r"^\s*(---|\*\*\*)\s*$", ln):
close_lists(); out.append("<hr>"); i += 1; continue
m = re.match(r"^(#{1,6})\s+(.*)$", ln)
if m:
close_lists(); lvl = len(m.group(1))
out.append(f"<h{lvl}>{inline(m.group(2))}</h{lvl}>"); i += 1; continue
if ln.startswith(">"):
close_lists(); buf = []
while i < len(lines) and lines[i].startswith(">"):
buf.append(lines[i].lstrip("> ").rstrip()); i += 1
out.append("<blockquote>" + inline(" ".join(buf)) + "</blockquote>"); continue
m = re.match(r"^(\s*)([-*+]|\d+\.)\s+(.*)$", ln)
if m:
tag = "ol" if m.group(2)[0].isdigit() else "ul"
if not list_stack or list_stack[-1] != tag:
close_lists(); out.append(f"<{tag}>"); list_stack.append(tag)
out.append(f"<li>{inline(m.group(3))}</li>"); i += 1; continue
if not ln.strip():
close_lists(); i += 1; continue
close_lists(); buf = []
while i < len(lines) and lines[i].strip() and not re.match(r"^(#|>|\s*\||\s*[-*+]\s|\s*\d+\.\s|```)", lines[i]):
buf.append(lines[i]); i += 1
out.append("<p>" + inline(" ".join(buf)) + "</p>")
close_lists()
return "\n".join(out)
def csv_table(path, limit=400):
rows = list(csv.reader(io.StringIO(read(path))))
if not rows: return "<p class='muted'>empty</p>"
head, body = rows[0], rows[1:limit]
t = ["<div class='scroll'><table class='data'><thead><tr>"
+ "".join(f"<th>{E(c)}</th>" for c in head) + "</tr></thead><tbody>"]
for r in body:
t.append("<tr>" + "".join(f"<td>{E(c)}</td>" for c in r) + "</tr>")
t.append("</tbody></table></div>")
if len(rows)-1 > limit:
t.append(f"<p class='muted'>showing first {limit} of {len(rows)-1} rows</p>")
return "".join(t)
# --------------------------------------------------------------------- photo captions
def photo_captions():
"""Pull the per-photo descriptions out of docs/05-photo-index.md so the gallery is annotated."""
caps = {}
for ln in read(rel("docs", "05-photo-index.md")).split("\n"):
m = re.match(r"^\|\s*`?(IMG_\d+[a-z_]*)`?\s*\|\s*\*?\*?([A-Z]{3})?\*?\*?\s*\|\s*(.*?)\s*\|", ln)
if m:
caps[m.group(1)] = (m.group(2) or "", m.group(3))
return caps
CAPS = photo_captions()
def gallery(folder, note=""):
files = sorted(f for f in os.listdir(rel(*folder)) if f.lower().endswith((".jpg", ".jpeg", ".png")))
cards = []
for f in files:
stem = os.path.splitext(f)[0]
room, desc = CAPS.get(stem, ("", ""))
src = "/".join(folder) + "/" + f
badge = f"<span class='pill'>{E(room)}</span>" if room else ""
cards.append(
f"<figure class='card' data-search='{E((f + ' ' + room + ' ' + desc).lower())}'>"
f"<img loading='lazy' src='{E(src)}' alt='{E(f)}' onclick=\"zoom('{E(src)}','{E(f)}')\">"
f"<figcaption><b>{E(stem)}</b> {badge}<br><span class='muted'>{E(desc) or '—'}</span>"
f"</figcaption></figure>")
head = f"<p class='muted'>{len(files)} images. Click any image to enlarge.{' ' + note if note else ''}</p>"
return head + "<div class='grid'>" + "".join(cards) + "</div>"
# --------------------------------------------------------------------- panes
PANES, NAV = {}, []
def pane(section, key, label, body, sub=""):
PANES[key] = f"<h1 class='pane-h'>{E(label)}</h1>" + (f"<p class='pane-s'>{sub}</p>" if sub else "") + body
NAV.append((section, key, label))
# --- overview
def overview():
import plan_data
rooms = plan_data.ROOMS
a = lambda r: (r[4]-r[2]) * (r[5]-r[3])
internal = sum(a(r) for r in rooms if r[0] != "BBL")
wet = sum(a(r) for r in rooms if r[0] in ("BTH", "ENS"))
oq = re.findall(r"^## (OQ-\d+) — (.*?)$", read(rel("docs", "02-open-questions.md")), re.M)
oq_rows = "".join(f"<tr><td class='mono'>{E(c)}</td><td>{md(t).replace('<p>','').replace('</p>','')}</td></tr>"
for c, t in oq)
return f"""
<div class='cards'>
<div class='stat'><b>Mon 17 Aug 2026</b><span>start on site (delayed from 13 Aug)</span></div>
<div class='stat'><b>Tue 17 Nov 2026</b><span>finish · 67 working days</span></div>
<div class='stat'><b>81</b><span>work items across 13 areas</span></div>
<div class='stat'><b>12</b><span>open questions, none answered</span></div>
<div class='stat'><b>{internal:.0f} m²</b><span>internal floor, measured</span></div>
<div class='stat'><b>{internal-wet:.0f} m²</b><span>less the wet rooms — cf. 125 m²</span></div>
</div>
<h2>What this is</h2>
<p>Kurt's refurbishment of the Zonqor apartment. <b>Roderick</b> (main contractor, with his son)
does gypsum, electrical, plumbing and tiling; <b>ULF</b> does carpentry; <b>Christine</b> walks the
site each evening, signs items off and agrees prices on paper. Print is the primary medium —
everything here is built to be printed.</p>
<h2>The two deliverables</h2>
<ul>
<li><b>Site Checklist</b> — 23 A4 pages: work plan, day-by-day, open questions, 13 room sections
with per-item price and sign-off fields, price summary, appendix of 49 photos.</li>
<li><b>Base Plan</b> — reconstructed floor plan, 12 rooms with areas and confidence grades, plus
the measure-on-site list. <b>A reconstruction, not a survey.</b></li>
</ul>
<h2>Open questions — all still unanswered</h2>
<table>{oq_rows}</table>
<h2>The three things to settle first</h2>
<ol>
<li><b>OQ-10 tiling scope</b> — before any price is agreed. The base plan shows the 125/145 m² gap
is the two balconies; what is still open is how much is actually being tiled (~22 m² of argument).</li>
<li><b>OQ-11 fireplace</b> — remove or keep? Needed by plan day 4, <b>Thu 20 Aug</b>.</li>
<li><b>OQ-03 consumer unit</b> and <b>OQ-08 intercom</b> — both change first-fix cable, week one.</li>
</ol>
"""
import sys
sys.path.insert(0, HERE)
pane("Start", "overview", "Zonqor Refurbishment — project index", overview(),
"Everything in this folder, in one place. Built "
+ datetime.date.today().strftime("%d %B %Y"))
# --- deliverables
# PDFs are shown as pre-rendered page images, NOT in an <iframe>. An iframe depends on the
# browser having a PDF viewer and on file:// letting it load — headless Chromium shows nothing,
# and Safari is unreliable. Page images always render. The real PDF is one click away.
PREVIEW = rel("output", "preview")
os.makedirs(PREVIEW, exist_ok=True)
def pdf_pages(pdf_path, stem, dpi=96):
src = rel(*pdf_path.split("/"))
have = sorted(f for f in os.listdir(PREVIEW) if f.startswith(stem + "-") and f.endswith(".jpg"))
if not have or os.path.getmtime(src) > min(os.path.getmtime(os.path.join(PREVIEW, f)) for f in have):
for f in have:
os.remove(os.path.join(PREVIEW, f))
os.system(f'pdftoppm -jpeg -r {dpi} "{src}" "{os.path.join(PREVIEW, stem)}"')
have = sorted(f for f in os.listdir(PREVIEW) if f.startswith(stem + "-") and f.endswith(".jpg"))
return have
for key, label, path, stem, note in [
("d-check", "Site Checklist (PDF)", "output/Zonqor Site Checklist.pdf", "checklist",
"The live document — this is what Christine prints and works from."),
("d-plan", "Base Plan (PDF)", "output/Zonqor Base Plan.pdf", "baseplan",
"Reconstructed floor plan, room schedule, and the measure-on-site list."),
("d-photo", "Photo Sort Sheet (PDF)", "output/Photo Sort Sheet.pdf", "photosort",
"Contact sheet used to try to place the 54 unassigned site photos."),
]:
pages = pdf_pages(path, stem)
imgs = "".join(
f"<figure class='page'><img loading='lazy' src='output/preview/{E(f)}' "
f"onclick=\"zoom('output/preview/{E(f)}','{E(label)} — page {n}')\">"
f"<figcaption class='muted'>page {n} of {len(pages)}</figcaption></figure>"
for n, f in enumerate(pages, 1))
pane("Deliverables", key, label,
f"<p class='muted'>{len(pages)} pages. {note} · "
f"<a href='{path}' target='_blank'><b>open the real PDF</b></a> "
f"(print from that, not from these previews). Click a page to enlarge.</p>"
f"<div class='pages'>{imgs}</div>")
pane("Deliverables", "d-html", "Site Checklist (HTML)",
"<p class='muted'>Same document, with live-totalling price boxes. "
"<a href='output/Zonqor Site Checklist.html' target='_blank'>Open full screen</a> to use it.</p>"
"<iframe class='pdf' src='output/Zonqor Site Checklist.html'></iframe>")
# --- docs
DOCS = [("CLAUDE.md", "CLAUDE.md — master context"), ("README.md", "README.md")]
DOCS += [(f"docs/{f}", f) for f in sorted(os.listdir(rel("docs"))) if f.endswith(".md")]
for p, label in DOCS:
pane("Documents", "doc-" + re.sub(r"\W+", "-", p), label, md(read(rel(p))))
# --- data
for f in sorted(os.listdir(rel("data"))):
fp = rel("data", f)
if f.endswith(".csv"):
pane("Data", "data-" + re.sub(r"\W+", "-", f), f,
f"<p class='muted'>data/{E(f)}</p>" + csv_table(fp))
elif f.endswith(".json"):
obj = json.loads(read(fp))
pane("Data", "data-" + re.sub(r"\W+", "-", f), f,
f"<p class='muted'>data/{E(f)} · {len(obj) if isinstance(obj, list) else len(obj.keys())} "
f"top-level entries</p><pre class='code'>{E(json.dumps(obj, indent=1)[:200000])}</pre>")
txt = sorted(os.listdir(rel("data", "pdf_text")))
pane("Data", "data-pdftext", "pdf_text/ — extracted drawing text",
"".join(f"<h3>{E(f)}</h3><pre class='code'>{E(read(rel('data','pdf_text',f)))}</pre>" for f in txt))
# --- images
pane("Images", "img-meas", "Measurement drawings",
gallery(("images", "measurements"),
"These are the dimensional source of truth. Two of them carry the scale bar the "
"base plan was calibrated on."))
pane("Images", "img-design", "Designer renders",
gallery(("images", "design"), "Visual intent only — no dimensions."))
pane("Images", "img-site", "Site photos (August 2026)",
gallery(("images", "site"),
"21 are placed in the checklist, 54 are not. Captions come from docs/05-photo-index.md."))
# --- build
for f in sorted(os.listdir(rel("build"))):
if f.endswith(".py"):
pane("Build scripts", "b-" + re.sub(r"\W+", "-", f), f,
f"<p class='muted'>build/{E(f)}</p><pre class='code'>{E(read(rel('build', f)))}</pre>")
# --------------------------------------------------------------------- assemble
sections, order = {}, []
for sec, key, label in NAV:
sections.setdefault(sec, []).append((key, label))
if sec not in order: order.append(sec)
nav_html = "".join(
f"<div class='nav-sec'>{E(sec)}</div>" +
"".join(f"<a class='nav-i' data-k='{k}' onclick=\"show('{k}')\">{E(l)}</a>" for k, l in sections[sec])
for sec in order)
panes_html = "".join(f"<section id='p-{k}' class='pane'>{v}</section>" for k, v in PANES.items())
HTML = f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>Zonqor Refurbishment — project index</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root {{ --bg:#fff; --fg:#111827; --mut:#6b7280; --line:#e5e7eb; --accent:#b45309;
--side:#f9fafb; --code:#f3f4f6; --sel:#fff7ed; }}
@media (prefers-color-scheme: dark) {{
:root {{ --bg:#0f1115; --fg:#e5e7eb; --mut:#9ca3af; --line:#272b33; --accent:#f59e0b;
--side:#151821; --code:#1a1e27; --sel:#2a1f0d; }}
}}
* {{ box-sizing:border-box; }}
body {{ margin:0; background:var(--bg); color:var(--fg); font:15px/1.6 -apple-system,
"Segoe UI", Helvetica, Arial, sans-serif; }}
#side {{ position:fixed; inset:0 auto 0 0; width:270px; background:var(--side);
border-right:1px solid var(--line); overflow-y:auto; padding:14px 0 40px; }}
#side h2 {{ font-size:14px; margin:0; padding:0 16px 10px; letter-spacing:.4px; }}
#side h2 span {{ color:var(--mut); font-weight:400; display:block; font-size:11px; }}
#q {{ width:calc(100% - 32px); margin:0 16px 12px; padding:7px 9px; font-size:13px;
border:1px solid var(--line); border-radius:6px; background:var(--bg); color:var(--fg); }}
.nav-sec {{ font-size:10.5px; text-transform:uppercase; letter-spacing:.8px; color:var(--mut);
padding:14px 16px 5px; }}
.nav-i {{ display:block; padding:5px 16px; font-size:13px; cursor:pointer; color:var(--fg);
text-decoration:none; border-left:3px solid transparent; }}
.nav-i:hover {{ background:var(--code); }}
.nav-i.on {{ background:var(--sel); border-left-color:var(--accent); font-weight:600; }}
#main {{ margin-left:270px; padding:26px 34px 90px; max-width:1100px; }}
.pane {{ display:none; }} .pane.on {{ display:block; }}
.pane-h {{ font-size:23px; margin:0 0 4px; }}
.pane-s {{ color:var(--mut); margin:0 0 18px; }}
h1,h2,h3,h4 {{ line-height:1.25; }}
h2 {{ font-size:17px; margin:26px 0 8px; border-bottom:1px solid var(--line); padding-bottom:5px; }}
h3 {{ font-size:14.5px; margin:18px 0 6px; }}
p, li {{ font-size:14px; }}
a {{ color:var(--accent); }}
table {{ border-collapse:collapse; width:100%; margin:10px 0 16px; font-size:13px; }}
th {{ text-align:left; border-bottom:2px solid var(--line); padding:6px 8px; font-size:11.5px;
text-transform:uppercase; letter-spacing:.4px; color:var(--mut); }}
td {{ border-bottom:1px solid var(--line); padding:6px 8px; vertical-align:top; }}
.scroll {{ overflow-x:auto; border:1px solid var(--line); border-radius:7px; margin:10px 0 16px; }}
.scroll table {{ margin:0; }}
table.data {{ font-size:11.5px; width:max-content; min-width:100%; }}
table.data td {{ white-space:nowrap; max-width:340px; overflow:hidden; text-overflow:ellipsis; }}
table.data th {{ position:sticky; top:0; background:var(--side); }}
code {{ background:var(--code); padding:1px 5px; border-radius:4px; font-size:12.5px; }}
pre.code {{ background:var(--code); padding:12px 14px; border-radius:7px; overflow-x:auto;
font-size:12px; line-height:1.45; }}
blockquote {{ border-left:3px solid var(--accent); background:var(--sel); margin:12px 0;
padding:9px 14px; border-radius:0 5px 5px 0; }}
blockquote p {{ margin:0; }}
hr {{ border:0; border-top:1px solid var(--line); margin:22px 0; }}
.muted {{ color:var(--mut); font-size:13px; }}
.mono {{ font-family:ui-monospace,Menlo,monospace; font-weight:600; white-space:nowrap; }}
iframe.pdf {{ width:100%; height:78vh; border:1px solid var(--line); border-radius:7px;
background:#fff; }}
.cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(155px,1fr)); gap:10px;
margin:6px 0 20px; }}
.stat {{ border:1px solid var(--line); border-radius:8px; padding:11px 13px; background:var(--side); }}
.stat b {{ display:block; font-size:19px; }}
.stat span {{ color:var(--mut); font-size:11.5px; }}
.grid {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(185px,1fr)); gap:13px; }}
.card {{ margin:0; border:1px solid var(--line); border-radius:8px; overflow:hidden;
background:var(--side); }}
.card img {{ width:100%; height:135px; object-fit:cover; display:block; cursor:zoom-in;
background:var(--code); }}
.card figcaption {{ padding:7px 9px; font-size:11.5px; line-height:1.4; }}
.pill {{ background:var(--accent); color:#fff; border-radius:4px; padding:0 5px; font-size:10px;
font-weight:700; }}
.pages {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(290px,1fr)); gap:16px; }}
.page {{ margin:0; }}
.page img {{ width:100%; border:1px solid var(--line); border-radius:5px; background:#fff;
cursor:zoom-in; display:block; }}
.page figcaption {{ padding-top:5px; font-size:11.5px; }}
#lb {{ position:fixed; inset:0; background:rgba(0,0,0,.92); display:none; z-index:99;
align-items:center; justify-content:center; flex-direction:column; cursor:zoom-out; }}
#lb.on {{ display:flex; }}
#lb img {{ max-width:94vw; max-height:88vh; object-fit:contain; }}
#lb div {{ color:#eee; font-size:13px; padding:10px; }}
@media (max-width:820px) {{ #side {{ position:static; width:auto; height:auto; }} #main {{ margin:0; padding:18px; }} }}
</style></head><body>
<nav id="side">
<h2>Zonqor Refurbishment<span>project index · {datetime.date.today().strftime('%d %b %Y')}</span></h2>
<input id="q" placeholder="Filter files…" oninput="filt(this.value)">
{nav_html}
</nav>
<main id="main">{panes_html}</main>
<div id="lb" onclick="this.classList.remove('on')"><img id="lbi"><div id="lbc"></div></div>
<script>
function show(k) {{
document.querySelectorAll('.pane').forEach(p => p.classList.remove('on'));
document.querySelectorAll('.nav-i').forEach(a => a.classList.remove('on'));
const p = document.getElementById('p-' + k); if (p) p.classList.add('on');
const a = document.querySelector('.nav-i[data-k="' + k + '"]'); if (a) a.classList.add('on');
document.getElementById('main').scrollTo(0, 0); window.scrollTo(0, 0);
history.replaceState(null, '', '#' + k);
}}
function zoom(src, cap) {{
document.getElementById('lbi').src = src;
document.getElementById('lbc').textContent = cap;
document.getElementById('lb').classList.add('on');
}}
function filt(v) {{
v = v.trim().toLowerCase();
document.querySelectorAll('.nav-i').forEach(a => {{
a.style.display = (!v || a.textContent.toLowerCase().includes(v)) ? '' : 'none';
}});
document.querySelectorAll('.nav-sec').forEach(s => {{
let n = s.nextElementSibling, any = false;
while (n && n.classList.contains('nav-i')) {{ if (n.style.display !== 'none') any = true; n = n.nextElementSibling; }}
s.style.display = any ? '' : 'none';
}});
document.querySelectorAll('.card').forEach(c => {{
c.style.display = (!v || (c.dataset.search || '').includes(v)) ? '' : 'none';
}});
}}
document.addEventListener('keydown', e => {{ if (e.key === 'Escape') document.getElementById('lb').classList.remove('on'); }});
// Without this, changing the hash (back/forward, a pasted deep link) leaves the pane behind.
window.addEventListener('hashchange', () => show(location.hash.slice(1) || 'overview'));
show(location.hash ? location.hash.slice(1) : 'overview');
</script></body></html>"""
out = rel("index.html")
open(out, "w", encoding="utf-8").write(HTML)
print(f"wrote {out} | {len(PANES)} panes, {len(HTML)/1024:.0f} KB")
build_plan.py
build/build_plan.py
"""Build the reconstructed base plan — print-first A4, same house style as the checklist.
python3 build_plan.py # writes ../output/base_plan.html
python3 pdfplan.py # renders ../output/Zonqor Base Plan.pdf
Geometry comes from plan_data.py and nowhere else. Print is the primary medium.
"""
import os, sys, datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from plan_data import (ROOMS, ENVELOPE, WALL_T, CEILING, BALCONY_ARC,
ALREADY_FINISHED, TO_MEASURE, SCALE_NOTE)
SCALE = 130 # 1:130 — the whole 28.7 m plus margins fits one A4 page
MM = 1000.0 / SCALE # mm on paper per metre
# viewBox in metres: x, y, width, height. The <svg> box below is derived from this so the
# rendered scale is EXACTLY 1:SCALE — if the two aspect ratios disagree the browser
# letterboxes and the printed scale silently stops matching the label on the sheet.
VB = (-1.6, -1.4, 12.6, 31.4)
CONF = {"A": ("#dbeafe", "read consistently off both sheets"),
"B": ("#fef3c7", "single reading — check on site"),
"C": ("#fee2e2", "inferred — must be confirmed")}
area = lambda r: (r[4]-r[2]) * (r[5]-r[3])
BAL_S = 15.8 # front balcony, traced arc
internal = sum(area(r) for r in ROOMS if r[0] != "BBL")
wet = sum(area(r) for r in ROOMS if r[0] in ("BTH", "ENS"))
bal_n = sum(area(r) for r in ROOMS if r[0] == "BBL")
# ----------------------------------------------------------------------- the drawing
def svg():
p = []
env = " ".join(f"{x},{y}" for x, y in ENVELOPE)
p.append(f'<polygon points="{env}" fill="#fff" stroke="#111" stroke-width="0.16"/>')
a = BALCONY_ARC
p.append(f'<path d="M 7.21,26.00 L {a["start"][0]},{a["start"][1]} '
f'A 5.4,5.4 0 0 1 {a["end"][0]},{a["end"][1]} '
f'L {a["west_edge"][0]},{a["west_edge"][1]} L 0,26.00 Z" '
f'fill="#fee2e2" stroke="#111" stroke-width="0.10"/>')
p.append('<text x="2.6" y="26.95" class="rc" text-anchor="middle">FBL</text>')
p.append('<text x="2.6" y="27.45" class="ra" text-anchor="middle">~15.8 m² · curved parapet</text>')
for code, name, x0, y0, x1, y1, cf, _ in ROOMS:
w, h = x1-x0, y1-y0
p.append(f'<rect x="{x0}" y="{y0}" width="{w}" height="{h}" fill="{CONF[cf][0]}" '
f'stroke="#111" stroke-width="0.07"/>')
cx, cy = x0+w/2, y0+h/2
a_m = area((code, name, x0, y0, x1, y1))
if w < 1.6: # too narrow for horizontal text — rotate it
p.append(f'<text x="{cx}" y="{cy}" class="rc" text-anchor="middle" '
f'transform="rotate(-90 {cx} {cy})">{code} · {w:.2f}×{h:.2f} · {a_m:.1f} m²</text>')
elif h < 1.15: # thin strip — label outside with a leader
p.append(f'<text x="{x1+0.35}" y="{cy+0.12}" class="rc">{code} · {a_m:.1f} m²</text>')
p.append(f'<line x1="{x1}" y1="{cy}" x2="{x1+0.3}" y2="{cy}" '
f'stroke="#111" stroke-width="0.03"/>')
else:
n_lines = 3 if h >= 2.0 else 2 # only fit the area line if the room is deep
p.append(f'<text x="{cx}" y="{cy-(0.25 if n_lines==3 else 0.1)}" class="rc" '
f'text-anchor="middle">{code}</text>')
p.append(f'<text x="{cx}" y="{cy+(0.28 if n_lines==3 else 0.42)}" class="ra" '
f'text-anchor="middle">{w:.2f} × {h:.2f}</text>')
if n_lines == 3:
p.append(f'<text x="{cx}" y="{cy+0.78}" class="ra" text-anchor="middle">'
f'{a_m:.1f} m²</text>')
# the two light wells on the west side
for lx, ly, lw, lh in [(0.0, 4.60, 2.19, 4.00), (0.0, 13.50, 2.43, 6.09)]:
p.append(f'<rect x="{lx-1.5}" y="{ly}" width="{lw+1.5}" height="{lh}" fill="#f3f4f6" '
f'stroke="#9ca3af" stroke-width="0.05" stroke-dasharray="0.25,0.18"/>')
p.append(f'<text x="{lx-1.35}" y="{ly+lh/2}" class="ra">light well</text>')
# overall dimensions
p.append('<line x1="0" y1="-0.75" x2="7.21" y2="-0.75" stroke="#b45309" stroke-width="0.05"/>')
p.append('<text x="3.6" y="-0.95" class="dim" text-anchor="middle">7.21 m overall</text>')
p.append('<line x1="8.0" y1="0" x2="8.0" y2="26.00" stroke="#b45309" stroke-width="0.05"/>')
p.append('<text x="8.35" y="13.0" class="dim" transform="rotate(90 8.35 13.0)" '
'text-anchor="middle">26.00 m overall (excl. front balcony)</text>')
# north arrow + scale bar
p.append('<path d="M 9.3,3.0 L 9.6,4.1 L 9.3,3.85 L 9.0,4.1 Z" fill="#111"/>')
p.append('<text x="9.3" y="2.7" class="ra" text-anchor="middle">N</text>')
for i in range(5):
p.append(f'<rect x="{8.9+i*0.4}" y="6.0" width="0.4" height="0.22" '
f'fill="{"#111" if i%2==0 else "#fff"}" stroke="#111" stroke-width="0.02"/>')
p.append('<text x="8.9" y="6.75" class="ra">0</text>')
p.append('<text x="10.9" y="6.75" class="ra" text-anchor="end">2 m</text>')
return "\n".join(p)
rows = "\n".join(
f'<tr><td class="c">{c}</td><td>{n}</td><td class="n">{x1-x0:.2f}</td>'
f'<td class="n">{y1-y0:.2f}</td><td class="n"><b>{area((c,n,x0,y0,x1,y1)):.1f}</b></td>'
f'<td class="cf cf{cf}">{cf}</td><td class="nt">{note}</td></tr>'
for c, n, x0, y0, x1, y1, cf, note in ROOMS)
measure = "\n".join(f'<tr><td class="c">{w}</td><td><b>{what}</b></td><td class="nt">{why}</td></tr>'
for w, what, why in TO_MEASURE)
HTML = f"""<meta charset="utf-8"><title>Zonqor Base Plan</title>
<style>
@page {{ size: A4; margin: 11mm 10mm 12mm 10mm; }}
* {{ box-sizing: border-box; }}
body {{ font: 9pt/1.35 "Helvetica Neue", Arial, sans-serif; color: #111; margin: 0; }}
h1 {{ font-size: 15pt; margin: 0 0 1mm; }}
h2 {{ font-size: 11pt; margin: 6mm 0 2mm; border-bottom: 1.2pt solid #111; padding-bottom: 1mm; }}
.sub {{ color: #555; margin-bottom: 3mm; }}
.warn {{ border: 1pt solid #b45309; background: #fffbeb; padding: 2.5mm 3mm; margin: 3mm 0;
font-size: 8.5pt; border-radius: 1mm; }}
.warn b {{ color: #b45309; letter-spacing: .3pt; }}
.wrap {{ display: flex; gap: 6mm; align-items: flex-start; }}
svg {{ flex: none; }}
.side {{ font-size: 8pt; }}
.key div {{ margin-bottom: 1.5mm; }}
.sw {{ display: inline-block; width: 4mm; height: 3mm; border: .5pt solid #111;
margin-right: 1.5mm; vertical-align: -.3mm; }}
table {{ width: 100%; border-collapse: collapse; font-size: 7.6pt; }}
th {{ text-align: left; border-bottom: 1pt solid #111; padding: 1.2mm 1mm; font-size: 7pt;
letter-spacing: .3pt; text-transform: uppercase; }}
td {{ border-bottom: .4pt solid #ddd; padding: 1.2mm 1mm; vertical-align: top; }}
td.c {{ font-family: monospace; font-weight: bold; white-space: nowrap; }}
td.n {{ text-align: right; font-variant-numeric: tabular-nums; }}
td.nt {{ color: #555; font-size: 7.2pt; }}
td.cf {{ text-align: center; font-weight: bold; }}
.cfA {{ background: #dbeafe; }} .cfB {{ background: #fef3c7; }} .cfC {{ background: #fee2e2; }}
.rc {{ font: bold 0.30px sans-serif; }}
.ra {{ font: 0.24px sans-serif; fill: #444; }}
.dim {{ font: 0.28px sans-serif; fill: #b45309; }}
.newpage {{ page-break-before: always; }}
.tot td {{ border-top: 1pt solid #111; font-weight: bold; background: #f9fafb; }}
</style>
<h1>Zonqor Apartment — Reconstructed Base Plan</h1>
<div class="sub">Traced from the designer's 1:100 layout · scale {SCALE and f"1:{SCALE}"} on this sheet ·
drawn {datetime.date.today().strftime('%d %B %Y')}</div>
<div class="wrap">
<svg width="{VB[2]*MM:.2f}mm" height="{VB[3]*MM:.2f}mm" viewBox="{VB[0]} {VB[1]} {VB[2]} {VB[3]}">
{svg()}
</svg>
<div class="side">
<div class="warn" style="margin-top:0"><b>⚠ A RECONSTRUCTION, NOT A SURVEY.</b>
Nobody has measured the flat. Every dimension here was scaled off the designer's furniture-layout
drawing, which itself carries the note <i>"All dimensions to be verified on site prior to
fabrication and installation."</i> Overall accuracy is about ±3% ({SCALE_NOTE}). Rooms shaded
amber or red are worse than that. <b>Do not order materials or cut anything to these figures.</b>
Use the plan to set out and to argue about scope; measure before you buy.</div>
<h2 style="margin-top:4mm">How to read it</h2>
<div class="key">
<div><span class="sw" style="background:#dbeafe"></span><b>A</b> — {CONF['A'][1]}</div>
<div><span class="sw" style="background:#fef3c7"></span><b>B</b> — {CONF['B'][1]}</div>
<div><span class="sw" style="background:#fee2e2"></span><b>C</b> — {CONF['C'][1]}</div>
</div>
<p>North is up. The flat is a single long bay, <b>7.21 m wide and 26.00 m deep</b>, with the front
balcony beyond the south wall. External walls measure <b>{WALL_T:.2f} m</b> throughout.</p>
<p>The west side steps in twice. Those two <b>light wells</b> are what the bathroom, study, ensuite
and entrance take their windows from — which is the strongest single check on this reconstruction,
because the site photos show windows in exactly those four rooms and nowhere else on that side.</p>
<p>Only one height is known: <b>{CEILING:.2f} m floor to ceiling</b> in the dining area
(830 + 1050 + 730 mm off the feature-wall elevation). Every other room is unmeasured.</p>
<h2>Where the scale came from</h2>
<p>The sheet <i>kids_bedroom_only</i> carries a printed scale bar: 10 m spans 599 px, with 1 m
ticks at exactly 60.0 px. The sheet <i>study_and_bedroom_revised</i> is the same drawing plotted
1.3137× larger, which works out to exactly 1:100 on A4. Two independent routes to the same scale,
agreeing to <b>0.08%</b>.</p>
</div>
</div>
<div class="newpage"></div>
<h2>Room schedule</h2>
<table>
<thead><tr><th>Code</th><th>Room</th><th style="text-align:right">Width m</th>
<th style="text-align:right">Depth m</th><th style="text-align:right">Floor m²</th>
<th style="text-align:center">Conf</th><th>Notes and what is uncertain</th></tr></thead>
<tbody>
{rows}
<tr class="tot"><td></td><td>Rooms measured above (excl. front balcony)</td><td></td><td></td>
<td class="n">{sum(area(r) for r in ROOMS):.1f}</td><td></td><td></td></tr>
</tbody></table>
<h2>What this settles about the tiling quantity (OQ-10)</h2>
<div class="warn" style="border-color:#166534; background:#f0fdf4">
<b style="color:#166534">THE 125 vs 145 m² GAP IS THE TWO BALCONIES.</b><br>
Measured off the plan, the internal rooms come to <b>{internal:.1f} m²</b>. Take out the main
bathroom and ensuite — which the site photos show are already finished — and you are left with
<b>{internal-wet:.1f} m²</b>. The designer's plan is marked <b>125 m²</b>. Those agree to 1.8%,
inside the tolerance of this reconstruction.<br><br>
Add the two balconies — back {bal_n:.1f} m² plus front ~{BAL_S:.1f} m² = <b>{bal_n+BAL_S:.1f} m²</b> —
and you land almost exactly on the <b>145 m²</b> in Roderick's quote. The gap between the two figures
is <b>{145-125} m²</b>, and the balconies are <b>{bal_n+BAL_S:.1f} m²</b>.<br><br>
<b>So the two numbers are not in conflict — they are measuring different things.</b> 125 m² is the
internal floor excluding the wet rooms; 145 m² is everything including balconies. The commercial
question is therefore not "which number is right" but <b>"how much of it is actually being
tiled?"</b> — and the photographs say the ensuite, the main bathroom and the front balcony are
already tiled. On that reading the new 120×120 area is nearer <b>{internal-wet:.0f} m²</b> than 145.
That is a difference of roughly <b>{145-(internal-wet):.0f} m²</b> to settle before any price is agreed.
</div>
<p style="font-size:8pt;color:#555">This is an arithmetic argument off a reconstructed plan, not a
measurement of the flat, and it does not tell you whether Roderick intended to re-tile the wet rooms.
It tells you what to ask him, and roughly how much money is in the answer.</p>
<h2>Measure these on site</h2>
<table>
<thead><tr><th>Where</th><th>What</th><th>Why it matters</th></tr></thead>
<tbody>
{measure}
</tbody></table>
<p style="font-size:8pt;color:#555;margin-top:4mm">Next: the electrical plan is drawn <i>on top of</i>
this base plan, so the items above should be checked before any point layout is committed. See
<code>docs/08-whats-missing.md</code> section B.</p>
"""
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "output", "base_plan.html")
open(out, "w").write(HTML)
print(f"wrote {os.path.normpath(out)} | {len(ROOMS)} rooms, "
f"internal {internal:.1f} m², internal less wet rooms {internal-wet:.1f} m²")
build_v3.py
build/build_v3.py
import base64, os, re, sys, datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rooms_data2 import ROOMS, ROOM_IMAGES, PHASES
IMG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img_print")
THUMB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img_thumb")
def b64(d, f):
with open(os.path.join(d, f), "rb") as fh:
return base64.b64encode(fh.read()).decode("ascii")
GROUP_ORDER = ["gypsum", "elec", "plumb", "tile", "fit"]
GROUP_LABEL = {"gypsum": "Gypsum / Structural", "elec": "Electrical",
"plumb": "Plumbing / Gas", "tile": "Tiling", "fit": "Fittings / Carpentry"}
ASSIGNED = {"gypsum": "Roderick", "elec": "Roderick", "plumb": "Roderick",
"tile": "Roderick", "fit": "ULF"}
# ----------------------------------------------------------- appendix numbering
img_id, img_order = {}, []
for name, code, _b, _i in ROOMS:
for fname, cap in ROOM_IMAGES.get(code, []):
if fname in img_id:
next(e for e in img_order if e[1] == fname)[3].append(code)
continue
img_id[fname] = "A%02d" % (len(img_order) + 1)
img_order.append((img_id[fname], fname, cap, [code]))
placeholder = {}
for name, code, _b, _i in ROOMS:
if not ROOM_IMAGES.get(code):
placeholder[code] = ("A%02d" % (len(img_order) + len(placeholder) + 1), name)
def ref_ids(code):
if code in placeholder:
return [placeholder[code][0]]
out = []
for fname, _c in ROOM_IMAGES.get(code, []):
if img_id[fname] not in out:
out.append(img_id[fname])
return out
def compress(ids):
if not ids: return ""
nums = sorted(int(i[1:]) for i in ids)
runs, s, p = [], nums[0], nums[0]
for n in nums[1:]:
if n == p + 1: p = n; continue
runs.append((s, p)); s = p = n
runs.append((s, p))
return ", ".join("A%02d" % a if a == b else "A%02d–A%02d" % (a, b) for a, b in runs)
# ------------------------------------------------------------------ item render
def default_dep(g, s1, s2, s3):
if g == "gypsum": return "start"
if g in ("elec", "plumb"): return "gypsum (this room)" if s1 else "start"
if g == "tile":
return "elec & plumbing rough-in (this room)" if s2 else ("gypsum (this room)" if s1 else "start")
if g == "fit":
if s3: return "tiling (this room)"
if s2: return "elec & plumbing rough-in (this room)"
if s1: return "gypsum (this room)"
return "start"
def render_item(it, dep, iid, code, ph):
a = ASSIGNED[it["group"]]
acls = "a-rod" if a == "Roderick" else "a-ulf"
note = f'<span class="i-note">{it["notes"]}</span>' if it["notes"] else ""
pht = f'<span class="phase-chip">Ph {ph}</span>' if ph else ""
return f''' <div class="wi" data-room="{code}">
<div class="wi-h"><span class="wi-id">{iid}</span><span class="wi-t">{it["title"]}</span>{pht}<span class="wi-a {acls}">{a}</span></div>
<div class="wi-m">{it["tags"]}<span class="dep">after: {dep}</span>{note}</div>
<div class="wi-f"><span class="fl">Status</span><label class="cb"><input type="checkbox" class="st-done" onchange="onDone(this)"><span>Done</span></label><label class="cb"><input type="checkbox"><span>Part</span></label><label class="cb"><input type="checkbox"><span>Issue</span></label><span class="fl fl2">Date</span><span class="wl"></span><span class="fl fl-p">Price €</span><input type="text" inputmode="decimal" class="price" data-room="{code}" oninput="recalc()"><span class="fl fl2">Init</span><span class="wl wl-s"></span></div>
<div class="wi-c"><span class="fl">Comments</span><span class="nl"></span></div>
</div>'''
def render_thumbs(code):
imgs = ROOM_IMAGES.get(code, [])
if not imgs:
aid, nm = placeholder[code]
return (f' <div class="rthumbs"><div class="thumb-none">'
f'<b>{aid}</b> — no designer picture for {nm}. Take an on-site photo; slot reserved in the appendix.'
f'</div></div>')
seen, cells = set(), []
for f, c in imgs:
if f in seen: continue
seen.add(f)
cells.append(f''' <figure class="tn" onclick="lb(this)"><img src="data:image/jpeg;base64,{b64(THUMB_DIR, f)}" alt="{c}" data-cap="{img_id[f]} — {c}"><figcaption>{img_id[f]}</figcaption></figure>''')
return ' <div class="rthumbs">\n' + "\n".join(cells) + '\n </div>'
def render_room(name, code, banner, items):
groups = {}
for it in items:
groups.setdefault(it["group"], []).append(it)
s1 = bool(groups.get("gypsum")); s2 = bool(groups.get("elec")) or bool(groups.get("plumb")); s3 = bool(groups.get("tile"))
ids = ref_ids(code)
refs = (f'<span class="rref rref-no">{ids[0]} — photo needed</span>' if code in placeholder
else f'<span class="rref">Pictures {compress(ids)}</span>')
parts = [render_thumbs(code)]
if banner:
parts.append(f' <div class="banner">{banner}</div>')
n = 1
for g in GROUP_ORDER:
if g not in groups: continue
parts.append(f' <div class="tgrp tg-{g}">{GROUP_LABEL[g]}</div>')
for it in groups[g]:
parts.append(render_item(it, it["dep_override"] or default_dep(g, s1, s2, s3),
f"{code}-{n:02d}", code, it.get("phase")))
n += 1
parts.append(f' <div class="rtot"><span class="fl">{name} — total agreed</span>'
f'<span class="rtv">€ <span class="rt" id="rt-{code}"></span></span></div>')
return f''' <section class="room">
<div class="rhead"><h2>{name}</h2><span class="rcode">{code}</span>{refs}<span class="rcount">{len(items)} items</span></div>
{chr(10).join(parts)}
</section>'''
rooms_html = "\n".join(render_room(n, c, b, i) for n, c, b, i in ROOMS)
summary_rows = "\n".join(
f'<tr><td class="s-code">{c}</td><td>{n}</td><td class="s-n">{len(i)}</td>'
f'<td class="s-val">€ <span class="rt" id="st-{c}"></span></td><td class="s-sign"></td></tr>'
for n, c, b, i in ROOMS)
# ------------------------------------------------------------------- appendix
ap = "\n".join(
f''' <figure class="ap" onclick="lb(this)"><img src="data:image/jpeg;base64,{b64(IMG_DIR, f)}" alt="{c}" data-cap="{aid} — {c}">
<figcaption><b>{aid}</b> {c} <span class="ap-r">[{", ".join(sorted(set(cs)))}]</span></figcaption></figure>'''
for aid, f, c, cs in img_order)
ph_html = "\n".join(
f''' <figure class="ap"><div class="photo-slot">Attach on-site photo<br>{nm}</div>
<figcaption><b>{aid}</b> {nm} — photo needed <span class="ap-r">[{cd}]</span></figcaption></figure>'''
for cd, (aid, nm) in placeholder.items())
# ------------------------------------------------------------------ day plan
START = datetime.date(2026, 8, 17) # Monday 17 August 2026 — delayed from Thu 13 Aug
# Never hardcode the start date in the HTML below; derive it from START so the two cannot drift.
DAYS = [
(1,"Site set-up: protect what stays, isolate services. Start lifting parquet — Open Plan.","WHS-01"),
(1,"Continue parquet lift — bedrooms and corridors.","WHS-01"),
(1,"Finish parquet lift. Strip out old ceiling lights, bedside lamps and window plugs.","WHS-01, MBR-02, MBR-03, MBR-04"),
(1,"Demolish dining feature wall + fireplace. <b>Check for live services in the wall first.</b>","OPN-01"),
(1,"Clear demolition debris, first skip out. Walk the back balcony and write in its scope.","WHS-02, BBL-01"),
(2,"Set out and confirm the new consumer unit position at the entrance (OQ-03 must be answered).","ENT-01"),
(2,"Install consumer unit, terminate mains. <b>Nothing else energises until this is in.</b>","ENT-01"),
(2,"Chase entrance and corridors. Pull intercom cable.","ENT-02, MCR-01, MCR-02"),
(2,"Open Plan kitchen zone — ceiling lights, hood, dedicated oven circuit.","OPN-03, OPN-04, OPN-05"),
(2,"Open Plan dining/living — cabinet lights, rope light, switches, plugs, TV point.","OPN-06 to OPN-10"),
(2,"Ethernet runs back to the entrance unit. Relocate double-pole switch.","OPN-11, OPN-12, OFF-08"),
(2,"Kitchen sink onto main feed, RO connection left ready.","OPN-13"),
(2,"Main bedroom first fix — ceiling, rope, bedside, vanity, wardrobe LED.","MBR-05 to MBR-09"),
(2,"Study/office first fix — ceiling, under-desk LED, plugs, switches, LED bar.","OFF-02 to OFF-07"),
(2,"Cy's bedroom first fix.","CYB-01 to CYB-05"),
(2,"Rafe's bedroom first fix.","RAF-01 to RAF-05"),
(2,"Main bathroom — gyser position, pipework and point. Ensuite switch route.","BTH-01"),
(2,"Front balcony — side lights, floor lights, switches, waterproof plugs, camera point.","FBL-01 to FBL-08"),
(2,"Front balcony gas line + pressure test and certificate.","FBL-09"),
(2,"Corridor emergency light short circuit. Test all existing feeds.","MCR-03, MCR-04, WHS-03"),
(2,"<b>First-fix sign-off walk.</b> Photograph every open chase before anything is closed up.","all first fix"),
(3,"Second skip — chasing debris out, sweep, floors prepped for tiling.","WHS-02"),
(4,"Set out and frame the study partition.","OFF-01"),
(4,"Study partition — rockwool insulation and inner two layers of board.","OFF-01"),
(4,"Study partition — outer board, tape and joint.","OFF-01"),
(4,"Small corridor partition extended +10cm for the new cabinets.","SCR-01"),
(4,"Gypsum behind sofa (250mm x 1m) including the rope-light housing.","OPN-02"),
(4,"Main bedroom — close off old ceiling light area.","MBR-01"),
(5,"Screed check and levelling — open plan.","WHS-04"),
(5,"Screed levelling — bedrooms and corridors.","WHS-04"),
(5,"Tank ensuite floor and shower walls. <b>Must be done before any ensuite tile.</b>","ENS-03"),
(5,"Set out 120x120 in the open plan — agree the starting line and cut positions with Christine.","WHS-05"),
(5,"Tile open plan — living/dining.","WHS-05"),
(5,"Tile open plan — kitchen end.","WHS-05"),
(5,"Tile entrance and main corridor.","WHS-05"),
(5,"Tile main corridor and small corridor.","WHS-05"),
(5,"Tile main bedroom.","WHS-05"),
(5,"Tile study and Cy's bedroom.","WHS-05"),
(5,"Tile Rafe's bedroom.","WHS-05"),
(5,"Tile front balcony — weather permitting.","WHS-05"),
(5,"Tile back balcony — weather permitting.","WHS-05"),
(5,"Ensuite 60x30 tiling — walls.","ENS-04"),
(5,"Ensuite tiling — floor, then grout.","ENS-04"),
(5,"Grout throughout, clean down, protect finished floors.","WHS-05"),
(6,"Patching and making good at all tile/wall junctions.","—"),
(6,"Finish patching, sand, prep every surface for paint.","—"),
(7,"Prime and seal all new gypsum.","—"),
(7,"Ceilings — bedrooms and study.","—"),
(7,"Ceilings — open plan and corridors. Ensuite ceiling.","ENS-01"),
(7,"Walls — first coat throughout.","—"),
(7,"Walls — second coat and cutting in.","—"),
(8,"Fit sockets and switches — open plan and entrance. Intercom unit on.","ENT-02"),
(8,"Fit sockets and switches — corridors and bedrooms.","—"),
(8,"Hang light fittings — open plan and corridors.","—"),
(8,"Hang light fittings — bedrooms and study. Rope and LED strips.","—"),
(8,"Wardrobe LED sensors, under-furniture LED, LED bar at study door.","MBR-09, CYB-03, RAF-03"),
(8,"<b>Full circuit test, RCD test, certification.</b> Confirm the oven no longer trips.","ENT-01, OPN-05"),
(9,"ULF starts wardrobes — main bedroom. Roderick on call for cut-ins.","MBR-11"),
(9,"ULF wardrobes — Cy's and Rafe's bedrooms.","—"),
(9,"Roderick cuts in gyser/pump switches and the ensuite switch.","MBR-10, ENS-02"),
(10,"Shower glass and tray; ensuite finished.","ENS-05"),
(10,"Bathroom sanitary — sink, cabinets, flush accessories, mirror.","BTH-02 to BTH-06"),
(10,"Mirrors — corridor 3x1.5m, office 2x2m, main bedroom vanity.","MCR-06, OFF-09, MBR-12"),
(10,"Outdoor wall finish — PVC or tiles, whichever OQ-01 decided.","FBL-10"),
(10,"Vent covers, PVC sheet on TV wall, remaining fittings.","MCR-05, OPN-14"),
(11,"<b>Snagging walk with Christine</b> against this checklist, room by room.","all"),
(11,"Fix snags, final clean, hand over certificates and warranties.","all"),
]
def workdays(start, n):
out, d = [], start
while len(out) < n:
if d.weekday() < 5:
out.append(d)
d += datetime.timedelta(days=1)
return out
dates = workdays(START, len(DAYS))
PH_NAMES = {
1:"Strip-out & demolition", 2:"First fix — plumbing, electrical, ethernet, gas",
3:"Clear site — skip", 4:"Structural gypsum (partitions)", 5:"Tiling",
6:"Gypsum amendments & making good", 7:"Painting", 8:"Second fix electrical",
9:"Carpentry — ULF (+ Roderick cut-ins)", 10:"Fittings, sanitary & outdoor",
11:"Snagging & handover"}
PH_CLS = {1:"p1",2:"p2",3:"p3",4:"p4",5:"p5",6:"p6",7:"p7",8:"p8",9:"p9",10:"p10",11:"p11"}
day_rows, last_ph = [], None
for i, ((ph, task, items), d) in enumerate(zip(DAYS, dates), 1):
if ph != last_ph:
day_rows.append(f'<tr class="ph-row {PH_CLS[ph]}"><td colspan="5">Phase {ph} — {PH_NAMES[ph]}</td></tr>')
last_ph = ph
wk = "" if i == 1 and d.weekday() != 0 else ""
day_rows.append(
f'<tr><td class="d-n">{i}</td><td class="d-d">{d.strftime("%a %d %b")}</td>'
f'<td class="d-p">{ph}</td><td>{task}</td><td class="d-i">{items}</td></tr>')
day_table = "\n".join(day_rows)
END = dates[-1]
ph_counts = {}
for ph, _t, _i in DAYS:
ph_counts[ph] = ph_counts.get(ph, 0) + 1
phase_rows = "\n".join(
f'<tr><td class="d-p">{p}</td><td>{PH_NAMES[p]}</td><td class="s-n">{ph_counts[p]} d</td>'
f'<td class="p-who">{"ULF + Roderick" if p == 9 else "Roderick + son"}</td></tr>'
for p in sorted(ph_counts))
OQ = [("OQ-01","Outdoor wall finish — PVC sheet or tiles?","Front Balcony","Before day 63"),
("OQ-02","Outdoor kitchen — confirm design / equipment spec","Front Balcony","Before day 18"),
("OQ-03","New consumer unit — confirm location and capacity","Entrance","<b>Before day 6</b>"),
("OQ-04","Study / Office plugs — confirm quantity","Study / Office","Before day 14"),
("OQ-05","Ensuite tiles — confirm 60x30 dimensions","Ensuite","Before day 42"),
("OQ-06","Living / Dining TV wall — confirm PVC sheet spec","Open Plan","Before day 64"),
("OQ-07","Sofa-area TV unit — confirm electrical point spec","Open Plan","Before day 10"),
("OQ-08","Intercom — reuse or replace? Audio or video?","Entrance","<b>Before day 8</b>"),
("OQ-09","Who books and pays for the skips?","Whole House","Before day 5"),
("OQ-10","Tiling scope — site photos show the ensuite, main bathroom and front balcony are <b>already tiled</b>. Does the ~145 sq m still stand, and what exactly is in it?","Whole House","<b>Before pricing</b>"),
("OQ-11","Fireplace — the site photo shows it installed and the wall already opened beneath it. Is it being removed, or kept with the wall reworked around it?","Open Plan","<b>Before day 4</b>"),
("OQ-12","Two tile formats are on site: Fanal 120 and Fanal 31.6x90. What is the 31.6x90 for — it is not in the work list.","Whole House","Before day 29")]
oq_rows = "\n".join(
f''' <div class="oq"><span class="wi-id">{i}</span><span class="wi-t">{t}</span>'''
f'''<span class="tag room">{r}</span><span class="tag status-tbc">{w}</span>'''
f'''<span class="oq-dec"><span class="fl">Decision & date</span><span class="nl"></span></span></div>'''
for i, t, r, w in OQ)
log_rows = "".join('<tr><td></td><td></td><td></td><td></td><td></td><td></td></tr>' for _ in range(16))
TOTAL = sum(len(i) for _, _, _, i in ROOMS)
html = f'''<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zonqor Refurbishment — Site Checklist, Price Sheet & Work Plan</title>
<style>
@page{{size:A4 portrait;margin:10mm 9mm 11mm;}}
:root{{--ink:#111827;--sub:#5b6472;--line:#d8d4cd;--hair:#9aa0a8;--accent:#8a4b09;--bg:#f6f5f2;}}
*{{box-sizing:border-box;}} html{{-webkit-print-color-adjust:exact;print-color-adjust:exact;}}
body{{margin:0;background:var(--bg);color:var(--ink);font-size:12.5px;line-height:1.35;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;padding:26px 14px 60px;}}
.sheet{{max-width:860px;margin:0 auto;background:#fff;border:1px solid var(--line);border-radius:6px;
box-shadow:0 1px 3px rgba(0,0,0,.07);padding:24px 26px 30px;}}
.bar{{max-width:860px;margin:0 auto 12px;display:flex;gap:10px;justify-content:space-between;align-items:center;flex-wrap:wrap;}}
.bar-l{{display:flex;gap:13px;align-items:center;flex-wrap:wrap;font-size:12px;color:var(--sub);}}
.btn{{border:1px solid var(--ink);background:var(--ink);color:#fff;padding:7px 14px;border-radius:6px;font-size:12.5px;font-weight:600;cursor:pointer;}}
.btn-2{{background:#fff;color:var(--ink);}} .btn:hover{{opacity:.86;}}
.tgl{{display:inline-flex;align-items:center;gap:5px;cursor:pointer;}}
.runtot{{font-weight:700;}} .runtot span{{color:var(--accent);}}
h1{{font-size:21px;margin:0 0 3px;letter-spacing:-.015em;}}
.cover{{border-bottom:2px solid var(--ink);padding-bottom:11px;margin-bottom:13px;}}
.cover .sub{{font-size:12.5px;color:var(--sub);}}
.cover .rev{{margin-top:6px;font-size:11.5px;color:var(--sub);}} .cover .rev b{{color:var(--ink);}}
.grid2{{display:grid;grid-template-columns:1fr 1fr;gap:12px;}}
.box{{border:1px solid var(--line);border-radius:5px;padding:9px 11px;background:#fcfbf9;}}
.box h3{{margin:0 0 5px;font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:var(--accent);}}
.box p{{margin:0 0 5px;font-size:11.5px;color:var(--sub);line-height:1.45;}} .box p:last-child{{margin:0;}}
.box ol,.box ul{{margin:0;padding-left:15px;font-size:11.5px;color:var(--sub);line-height:1.5;}}
.who{{display:flex;gap:7px;font-size:11.5px;margin-bottom:3px;}} .who b{{min-width:64px;}}
.signrow{{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:12px;}}
.sig{{font-size:11px;color:var(--sub);}} .sig .sl{{border-bottom:1px solid var(--hair);height:22px;margin-bottom:3px;}}
.sec{{font-size:14.5px;margin:0 0 2px;}} .secsub{{font-size:11.5px;color:var(--sub);margin-bottom:9px;}}
.warn{{border:1px solid #e0b48a;background:#fdf3e8;border-radius:5px;padding:9px 11px;margin-bottom:9px;}}
.warn h3{{margin:0 0 4px;font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#9a4a06;}}
.warn p{{margin:0 0 5px;font-size:11.5px;color:#6b4520;line-height:1.45;}} .warn p:last-child{{margin:0;}}
.room{{margin-top:13px;}}
.rhead{{display:flex;align-items:baseline;gap:7px;border-bottom:1.4px solid var(--ink);padding-bottom:3px;margin-bottom:5px;flex-wrap:wrap;}}
.rhead h2{{font-size:12.5px;margin:0;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);}}
.rcode{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;font-weight:700;background:#eceae6;color:#3f4652;padding:1px 5px;border-radius:3px;}}
.rref{{font-size:10px;color:var(--sub);font-style:italic;}} .rref-no{{color:#a1620c;}}
.rcount{{margin-left:auto;font-size:10.5px;color:var(--sub);}}
.rthumbs{{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px;}}
.tn{{margin:0;width:74px;cursor:pointer;}}
.tn img{{width:74px;height:52px;object-fit:cover;border:1px solid var(--line);border-radius:3px;display:block;}}
.tn figcaption{{font-size:8.5px;font-weight:700;color:var(--sub);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center;margin-top:1px;}}
.thumb-none{{font-size:10.5px;color:#a1620c;background:#fdf6e7;border:1px dashed #e0b48a;border-radius:4px;padding:5px 8px;width:100%;}}
.banner{{font-size:11px;color:#7c4a03;background:#fdf6e7;border:1px solid #e8d9b4;border-radius:4px;padding:5px 8px;margin-bottom:5px;}}
.tgrp{{font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);margin:8px 0 4px;border-bottom:1px solid var(--line);padding-bottom:2px;}}
.tg-gypsum{{color:#6b21a8;}} .tg-elec{{color:#8a5a06;}} .tg-plumb{{color:#1d4ed8;}} .tg-tile{{color:#9f1239;}} .tg-fit{{color:#0369a1;}}
.wi{{border:1px solid var(--line);border-radius:4px;padding:5px 8px 6px;margin-bottom:4px;}}
.wi-h{{display:flex;gap:7px;align-items:baseline;}}
.wi-id{{flex:0 0 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9.5px;font-weight:700;color:#3f4652;background:#eceae6;border-radius:3px;padding:1px 5px;}}
.wi-t{{flex:1 1 auto;font-size:12.5px;font-weight:700;line-height:1.25;}}
.phase-chip{{flex:0 0 auto;font-size:9px;font-weight:700;background:#eef2f7;color:#3d4c63;border:.5px solid #d2dae5;border-radius:3px;padding:1px 5px;white-space:nowrap;}}
.wi-a{{flex:0 0 auto;font-size:9.5px;font-weight:700;padding:1px 7px;border-radius:999px;white-space:nowrap;}}
.a-rod{{background:#dff3e2;color:#15603a;border:.5px solid #b6e0c1;}} .a-ulf{{background:#fdeada;color:#8a3f0d;border:.5px solid #f2d3ae;}}
.wi-m{{display:flex;gap:5px;flex-wrap:wrap;align-items:baseline;margin-top:2px;}}
.tag{{font-size:9.5px;padding:.5px 6px;border-radius:999px;background:#f4efe6;color:var(--accent);font-weight:600;white-space:nowrap;border:.5px solid #e5dcc9;}}
.tag.trade-elec{{background:#fdf6d8;color:#7a4f05;border-color:#eddfa8;}}
.tag.trade-plumb{{background:#e3ecfd;color:#1d4ed8;border-color:#c3d4f7;}}
.tag.trade-gypsum{{background:#f4e9fd;color:#6b21a8;border-color:#e0cef7;}}
.tag.trade-tile{{background:#fde5e9;color:#9f1239;border-color:#f6ccd4;}}
.tag.trade-fit{{background:#e2f1fb;color:#0369a1;border-color:#c4e0f2;}}
.tag.status-remove{{background:#fde4e4;color:#991b1b;border-color:#f5c7c7;}}
.tag.status-known{{background:#fdeaea;color:#b91c1c;border-color:#f3c2c2;}}
.tag.status-tbc{{background:#eef1f4;color:#44506a;border-color:#dbe1e8;}}
.tag.status-new{{background:#e6f6ec;color:#15603a;border-color:#bfe3cb;}}
.dep{{font-size:10px;color:#7b6f5f;font-style:italic;}}
.i-note{{font-size:10.5px;color:var(--sub);flex:1 1 100%;line-height:1.3;}}
.wi-f{{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:5px;padding-top:4px;border-top:1px dashed var(--line);}}
.fl{{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);}}
.fl2{{margin-left:9px;}} .fl-p{{margin-left:9px;color:var(--accent);}}
.cb{{display:inline-flex;align-items:center;gap:3px;font-size:10.5px;cursor:pointer;}}
.cb input{{width:12px;height:12px;margin:0;accent-color:var(--accent);cursor:pointer;}}
.wl{{display:inline-block;width:60px;border-bottom:1px solid var(--hair);height:13px;}} .wl-s{{width:34px;}}
.price{{width:70px;border:none;border-bottom:1px solid var(--hair);background:transparent;font:inherit;font-size:12px;font-weight:700;text-align:right;padding:0 2px 1px;}}
.price:focus{{outline:none;border-bottom-color:var(--accent);background:#fdf8ef;}}
.wi-c{{display:flex;align-items:baseline;gap:6px;margin-top:4px;}}
.nl{{flex:1 1 auto;border-bottom:1px solid var(--line);height:13px;}}
.wi.is-done{{background:#f6fbf6;border-color:#bfe0c6;}} .wi.is-done .wi-t{{color:#3f6b4a;}}
.rtot{{display:flex;justify-content:flex-end;gap:8px;align-items:baseline;margin-top:5px;padding-top:4px;border-top:1.4px solid var(--ink);}}
.rtv{{font-size:12.5px;font-weight:800;}}
.rt{{display:inline-block;min-width:62px;border-bottom:1px solid var(--hair);text-align:right;padding:0 3px;}}
.oq{{display:flex;gap:7px;align-items:baseline;flex-wrap:wrap;border:1px solid #e8d9b4;background:#fefaf1;border-radius:4px;padding:6px 9px;margin-bottom:4px;}}
.oq .wi-t{{flex:0 1 auto;}}
.oq-dec{{flex:1 1 100%;display:flex;align-items:baseline;gap:6px;margin-top:2px;}}
.apgrid{{display:grid;grid-template-columns:repeat(3,1fr);gap:11px;}}
.ap{{margin:0;cursor:pointer;}}
.ap img{{width:100%;height:118px;object-fit:cover;border:1px solid var(--line);border-radius:4px;display:block;}}
.ap figcaption{{font-size:9.5px;color:var(--sub);line-height:1.3;margin-top:3px;}}
.ap figcaption b{{color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}}
.ap-r{{color:#a1620c;}}
.photo-slot{{border:1px dashed var(--hair);border-radius:4px;height:118px;display:flex;align-items:center;justify-content:center;font-size:9.5px;color:var(--hair);text-transform:uppercase;letter-spacing:.05em;text-align:center;padding:6px;}}
table.sum,table.log,table.plan,table.day{{width:100%;border-collapse:collapse;font-size:11.5px;}}
table th{{text-align:left;font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);border-bottom:1.3px solid var(--ink);padding:4px 5px;font-weight:800;}}
table td{{border-bottom:1px solid var(--line);padding:5px;vertical-align:top;}}
table.log td,table.log th{{border-right:1px solid var(--line);}}
table.log td:last-child,table.log th:last-child{{border-right:none;}} table.log td{{height:26px;}}
.s-code{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:700;font-size:10px;width:42px;}}
.s-n{{width:46px;color:var(--sub);}} .s-val{{width:112px;text-align:right;font-weight:700;white-space:nowrap;}}
.s-sign{{width:78px;}}
table.sum tfoot td{{border-top:1.8px solid var(--ink);border-bottom:none;padding-top:7px;font-weight:800;font-size:12.5px;}}
.lg-d{{width:58px;}} .lg-w{{width:68px;}} .lg-i{{width:104px;}} .lg-a{{width:66px;}} .lg-s{{width:48px;}}
.d-n{{width:30px;font-weight:700;color:var(--sub);font-size:10.5px;}}
.d-d{{width:74px;font-weight:700;white-space:nowrap;font-size:11px;}}
.d-p{{width:26px;color:var(--sub);font-size:10.5px;}}
.d-i{{width:132px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9.5px;color:var(--sub);}}
.p-who{{width:110px;color:var(--sub);}}
tr.ph-row td{{background:#eef2f7;font-weight:800;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#2f3b4d;padding:4px 5px;border-bottom:1px solid var(--line);}}
.lb{{display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:99;align-items:center;justify-content:center;flex-direction:column;padding:22px;}}
.lb.on{{display:flex;}} .lb img{{max-width:100%;max-height:84vh;border-radius:5px;}}
.lb .cap{{color:#fff;font-size:12.5px;margin-top:9px;text-align:center;}}
.lb .x{{position:absolute;top:14px;right:20px;background:none;border:none;color:#fff;font-size:28px;cursor:pointer;}}
@media(max-width:700px){{.grid2,.signrow{{grid-template-columns:1fr;}}.apgrid{{grid-template-columns:repeat(2,1fr);}}.sheet{{padding:16px 13px;}}}}
@media print{{
body{{background:#fff;padding:0;font-size:8.7pt;line-height:1.28;}}
.sheet{{max-width:none;border:none;border-radius:0;box-shadow:none;padding:0;}}
.bar,.lb,.noprint{{display:none!important;}}
h1{{font-size:16pt;}} .sec{{font-size:12pt;}}
.newpage{{break-before:page;}}
.wi,.oq,.ap,.box,.warn,.rtot,.rhead,tr{{break-inside:avoid;}}
.rhead,.tgrp,.sec,.secsub,.rthumbs{{break-after:avoid;}}
thead{{display:table-header-group;}}
.wi{{padding:3.6pt 5pt 4pt;margin-bottom:2.6pt;border-color:#bbb6ae;}}
.wi-t{{font-size:9.4pt;}} .tag{{font-size:6.8pt;}} .dep{{font-size:7.2pt;}} .i-note{{font-size:7.6pt;}}
.wi-a,.phase-chip{{font-size:6.8pt;}} .fl{{font-size:6.6pt;}} .cb{{font-size:7.6pt;}}
.cb input{{width:8.6pt;height:8.6pt;}}
.nl{{height:6.4mm;border-bottom-color:#9d988f;}}
.wl{{height:3.6mm;border-bottom-color:#8b867e;width:17mm;}} .wl-s{{width:10mm;}}
.price{{border-bottom-color:#8b867e;width:20mm;height:3.6mm;}}
.rt{{border-bottom-color:#8b867e;min-width:22mm;}}
.room{{margin-top:9pt;}}
.tn{{width:19mm;}} .tn img{{width:19mm;height:13mm;}} .tn figcaption{{font-size:6pt;}}
.ap img{{height:30mm;}} .ap figcaption{{font-size:6.6pt;}} .photo-slot{{height:30mm;font-size:6.6pt;}}
.apgrid{{gap:5mm 4mm;}}
table.log td{{height:10.5mm;}}
table.day td{{padding:2.6pt 4pt;}}
body.no-app .appendix{{display:none;}}
}}
</style></head><body>
<div class="bar noprint">
<div class="bar-l">
<span class="runtot">Agreed so far: €<span id="grand">0</span></span>
<span id="donecount"></span>
<label class="tgl"><input type="checkbox" id="tApp" checked onchange="tog()"> Print picture appendix</label>
</div>
<div><button class="btn btn-2" onclick="clearAll()">Clear entries</button>
<button class="btn" onclick="window.print()">Print / Save as PDF</button></div>
</div>
<div class="sheet">
<div class="cover">
<h1>Zonqor Apartment — Site Checklist, Prices & Work Plan</h1>
<div class="sub">Turnkey refurbishment — Roderick and son, with ULF on carpentry</div>
<div class="rev"><b>Revision:</b> <span id="revdate"></span> · <b>{TOTAL} items</b> across {len(ROOMS)} areas ·
<b>Start {START.strftime('%a %d %b %Y')}</b>, {len(DAYS)} working days (Mon–Fri) to <b>{END.strftime('%a %d %b %Y')}</b> ·
Rooms follow the walk-through order · Pictures in the <b>Appendix</b>, referenced A01…</div>
</div>
<div class="grid2">
<div class="box"><h3>Who does what</h3>
<div class="who"><b>Roderick</b><span>Gypsum & structural, electrical, plumbing & gas, tiling — with his son, 2 on site</span></div>
<div class="who"><b>ULF</b><span>Carpentry, wardrobes, fittings, mirrors, accessories</span></div>
<div class="who"><b>Christine</b><span>Daily walk-through, sign-off, price agreement</span></div>
<div class="who"><b>Kurt</b><span>Open questions, specs, ordering decisions</span></div>
</div>
<div class="box"><h3>How to use this sheet</h3>
<ol><li>Find the item by its <b>ID code</b> (e.g. MBR-05). Pictures are the small <b>A</b> numbers — full size in the appendix.</li>
<li>Tick <b>Done</b>, <b>Part</b> or <b>Issue</b>; write the date.</li>
<li>Agree the price, write it in <b>Price €</b>, initial the row.</li>
<li><b>Ph</b> on each item is its phase in the work plan.</li>
<li>Carry room totals to the <b>Price Summary</b> at the back.</li></ol>
</div>
</div>
<div class="box" style="margin-top:10px;"><h3>Before signing off an item</h3>
<p>An item is only <b>Done</b> when the work is finished <em>and</em> the price is agreed and written down. If the price
is still being discussed, tick <b>Part</b> and say why — never leave the price blank on a completed item. Anything
marked <b>Issue</b> goes to Kurt the same evening. An item whose “after:” line names an <b>OQ</b> number
cannot start until that question is decided.</p></div>
<div class="signrow">
<div class="sig"><div class="sl"></div>Christine — signature & date</div>
<div class="sig"><div class="sl"></div>Roderick — signature & date</div>
<div class="sig"><div class="sl"></div>ULF — signature & date</div>
</div>
<!-- ================= WORK PLAN ================= -->
<section class="newpage" style="margin-top:16px;">
<h2 class="sec">Work Plan — phases</h2>
<div class="secsub">Built on the order Roderick proposed, with the missing stages filled in and one sequencing change (below).</div>
<div class="warn"><h3>⚠ One change to the proposed order — gypsum before tiling, not after</h3>
<p>Roderick's order puts <b>tiling before gypsum</b>. That works for patching and making good, but not for the
<b>study partition (OFF-01)</b>, the <b>small corridor extension (SCR-01)</b> or the <b>sofa wall (OPN-02)</b> —
those are full walls. Tiling under a wall that is then built on top wastes 120x120 tile, and the wall sits on a
tiled surface instead of the slab.</p>
<p>So the plan splits it: <b>Phase 4</b> builds the partitions <em>before</em> tiling, and <b>Phase 6</b> keeps the
patching and making good <em>after</em>. Worth confirming with Roderick on day one — he may have a reason,
but if not, this saves tile and gives a better junction.</p></div>
<div class="warn"><h3>⚠ What was missing from the proposed order</h3>
<p>The proposed sequence stopped at gypsum. Six stages were not listed and are now in the plan:
<b>painting</b> (Ph 7), <b>second fix electrical</b> (Ph 8 — fitting and terminating every point, hanging
fittings, then testing and certification), <b>carpentry</b> (Ph 9 — ULF, with Roderick returning to cut in the
gyser/pump and ensuite switches), <b>fittings and sanitary</b> (Ph 10), <b>the outdoor/balcony works</b> (Ph 10) and
<b>snagging and handover</b> (Ph 11).</p>
<p>Three items were also missing from the work list itself and have been added:
<b>WHS-01</b> strip out the old parquet, <b>WHS-04</b> level the screed once it is up (120x120 is unforgiving on an
uneven floor), <b>WHS-02</b> skip hire, and <b>ENS-03</b> tanking the ensuite wet area before it is tiled.</p></div>
<table class="plan">
<thead><tr><th>Ph</th><th>Phase</th><th>Days</th><th>Crew</th></tr></thead>
<tbody>
{phase_rows}
</tbody>
<tfoot><tr><td></td><td><b>Total — {START.strftime('%a %d %b')} to {END.strftime('%a %d %b %Y')}</b></td>
<td class="s-n"><b>{len(DAYS)} d</b></td><td></td></tr></tfoot>
</table>
<div class="box" style="margin-top:10px;"><h3>Assumptions behind these durations</h3>
<p>Two people on site, Monday to Friday, no public holidays deducted. Roderick's crew does not do painting on some
jobs — if painting is not in his price, Phase 7 is a separate trade and the dates after it shift.
ULF's carpentry (Ph 9) is shown sequentially but can start earlier if the rooms are tiled and painted; that
would pull the finish date in by roughly a week. Balcony tiling assumes dry weather.
Nothing here is committed until Roderick agrees it — walk the plan with him on day one.</p></div>
</section>
<section class="newpage" style="margin-top:16px;">
<h2 class="sec">Day-by-Day Plan</h2>
<div class="secsub">{START.strftime('%a %d %b %Y')} → {END.strftime('%a %d %b %Y')} · {len(DAYS)} working days, Mon–Fri · Roderick + son unless noted.</div>
<table class="day">
<thead><tr><th>Day</th><th>Date</th><th>Ph</th><th>Work</th><th>Items</th></tr></thead>
<tbody>
{day_table}
</tbody>
</table>
</section>
<section class="newpage" style="margin-top:16px;">
<h2 class="sec">Open Questions — decide before ordering</h2>
<div class="secsub">These block the items that name them. The deadline column is the day in the plan by which the answer is needed.</div>
{oq_rows}
</section>
{rooms_html}
<section class="newpage" style="margin-top:16px;">
<h2 class="sec">Price Summary</h2>
<div class="secsub">Carry each room total across. Agreed totals only — anything still under discussion stays out until settled.</div>
<table class="sum">
<thead><tr><th>Code</th><th>Area</th><th>Items</th><th style="text-align:right;">Total agreed</th><th>Signed</th></tr></thead>
<tbody>
{summary_rows}
</tbody>
<tfoot><tr><td></td><td>Grand total agreed</td><td></td><td class="s-val">€ <span class="rt" id="st-GRAND"></span></td><td></td></tr></tfoot>
</table>
<div class="signrow" style="margin-top:16px;">
<div class="sig"><div class="sl"></div>Christine — signature & date</div>
<div class="sig"><div class="sl"></div>Roderick — signature & date</div>
<div class="sig"><div class="sl"></div>ULF — signature & date</div>
</div>
</section>
<section style="margin-top:16px;">
<h2 class="sec">Daily Site Log</h2>
<div class="secsub">One line per visit — deliveries, delays, extra labour, verbal agreements, anything not tied to a single item.</div>
<table class="log">
<thead><tr><th class="lg-d">Date</th><th class="lg-w">On site</th><th class="lg-i">Items (IDs)</th>
<th class="lg-a">Agreed €</th><th class="lg-n">Notes / issues raised</th><th class="lg-s">Init.</th></tr></thead>
<tbody>{log_rows}</tbody>
</table>
</section>
<section class="newpage appendix" style="margin-top:16px;">
<h2 class="sec">Appendix — Reference Pictures</h2>
<div class="secsub">Full-size versions of the thumbnails shown in each room. The code in brackets is the room(s) each picture serves.</div>
<div class="apgrid">
{ap}
{ph_html}
</div>
</section>
</div>
<div class="lb" id="lb" onclick="if(event.target===this)lbx()">
<button class="x" onclick="lbx()">×</button><img id="lbimg" src="" alt=""><div class="cap" id="lbcap"></div>
</div>
<script>
document.getElementById('revdate').textContent=new Date().toLocaleDateString('en-GB',{{year:'numeric',month:'long',day:'numeric'}});
function money(n){{return n.toLocaleString('en-MT',{{minimumFractionDigits:2,maximumFractionDigits:2}});}}
function recalc(){{
var byRoom={{}},grand=0;
document.querySelectorAll('.price').forEach(function(i){{
var v=parseFloat(String(i.value).replace(/[^0-9.\\-]/g,'')); if(isNaN(v))return;
var r=i.getAttribute('data-room'); byRoom[r]=(byRoom[r]||0)+v; grand+=v;}});
document.querySelectorAll('.rt').forEach(function(el){{
if(el.id==='st-GRAND'){{el.textContent=grand?money(grand):'';return;}}
var c=el.id.replace(/^(rt|st)-/,''); el.textContent=byRoom[c]?money(byRoom[c]):'';}});
document.getElementById('grand').textContent=money(grand);
}}
function dc(){{var a=document.querySelectorAll('.st-done').length,d=document.querySelectorAll('.st-done:checked').length;
document.getElementById('donecount').textContent=d+' / '+a+' items done';}}
function onDone(cb){{cb.closest('.wi').classList.toggle('is-done',cb.checked);dc();}}
function tog(){{document.body.classList.toggle('no-app',!document.getElementById('tApp').checked);}}
function clearAll(){{
document.querySelectorAll('.price').forEach(function(i){{i.value='';}});
document.querySelectorAll('input[type=checkbox]').forEach(function(c){{
if(c.id==='tApp')return; c.checked=false;
if(c.classList.contains('st-done'))c.closest('.wi').classList.remove('is-done');}});
recalc();dc();}}
function lb(f){{var i=f.querySelector('img'); if(!i)return;
document.getElementById('lbimg').src=i.src;
document.getElementById('lbcap').textContent=i.getAttribute('data-cap')||'';
document.getElementById('lb').classList.add('on');}}
function lbx(){{document.getElementById('lb').classList.remove('on');}}
document.addEventListener('keydown',function(e){{if(e.key==='Escape')lbx();}});
recalc();dc();
</script></body></html>
'''
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),'..','output','renovation_checklist.html'), 'w') as f:
f.write(html)
print("bytes:", len(html))
print("items:", TOTAL, "| appendix:", len(img_order), "+", len(placeholder), "slots")
print("plan:", len(DAYS), "working days,", START, "->", END)
gen_day_plan.py
build/gen_day_plan.py
"""Regenerate docs/04-day-by-day.md from the built checklist.
The day plan lives in build_v3.py (START + DAYS). This mirrors the *rendered* table into
markdown so the doc can never drift from the document Christine prints. Run it after any
change to START or DAYS:
python3 build_v3.py && python3 gen_day_plan.py
"""
import os, re, sys
from bs4 import BeautifulSoup
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "output", "renovation_checklist.html")
OUT = os.path.join(HERE, "..", "docs", "04-day-by-day.md")
soup = BeautifulSoup(open(SRC).read(), "html.parser")
def md(cell):
"""Cell text as markdown — <b> becomes **bold**, everything else flattened."""
for b in cell.find_all("b"):
b.replace_with(f"**{b.get_text(' ', strip=True)}**")
return re.sub(r"\s+", " ", cell.get_text(" ", strip=True)).replace(" **", " **")
table = soup.select_one("table.day")
rows = table.select("tr")
days = [r for r in rows if r.select_one(".d-n")]
first, last = days[0].select_one(".d-d").get_text(strip=True), days[-1].select_one(".d-d").get_text(strip=True)
# The day plan's own subtitle — not the first .secsub on the page, which belongs to the Work Plan.
span = table.find_previous(class_="secsub").get_text(" ", strip=True).split("·")[0].strip()
out = ["# Day-by-day plan", "",
f"**{span} · {len(days)} working days, Mon–Fri.**",
"Roderick + son unless noted. Generated by `build/gen_day_plan.py` from the built",
"checklist — regenerate rather than hand-edit.", ""]
for r in rows:
head = r.find("td", colspan="5")
if head:
out += ["", f"### {head.get_text(' ', strip=True)}", "",
"| Day | Date | Work | Items |", "|---|---|---|---|"]
elif r.select_one(".d-n"):
n = r.select_one(".d-n").get_text(strip=True)
d = r.select_one(".d-d").get_text(strip=True)
work = md(r.select_one(".d-d").find_next_sibling("td").find_next_sibling("td"))
items = r.select_one(".d-i").get_text(" ", strip=True)
out.append(f"| {n} | {d} | {work} | `{items}` |")
open(OUT, "w").write("\n".join(out) + "\n")
print(f"wrote {OUT}: {len(days)} days, {first} -> {last}")
pdfcheck.py
build/pdfcheck.py
"""Render the checklist HTML to A4 PDF via Playwright/Chromium.
Print is the primary medium — always eyeball the rendered pages, don't assume the CSS worked."""
import os
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "output", "renovation_checklist.html")
OUT = os.path.join(HERE, "..", "output", "Zonqor Site Checklist.pdf")
# The Cowork sandbox shipped chromium at a fixed path; on a normal machine let
# Playwright resolve its own install (~/Library/Caches/ms-playwright on macOS).
SANDBOX_CHROMIUM = "/opt/pw-browsers/chromium"
launch_kw = {"executable_path": SANDBOX_CHROMIUM} if os.path.exists(SANDBOX_CHROMIUM) else {}
with sync_playwright() as p:
b = p.chromium.launch(**launch_kw)
pg = b.new_page()
pg.goto("file://" + os.path.abspath(SRC))
pg.wait_for_timeout(900)
pg.pdf(path=OUT, format="A4", print_background=True,
margin={"top": "11mm", "bottom": "12mm", "left": "10mm", "right": "10mm"})
b.close()
print("wrote", OUT)
pdfplan.py
build/pdfplan.py
"""Render the base plan HTML to A4 PDF. Same launcher logic as pdfcheck.py."""
import os
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "output", "base_plan.html")
OUT = os.path.join(HERE, "..", "output", "Zonqor Base Plan.pdf")
SANDBOX_CHROMIUM = "/opt/pw-browsers/chromium"
launch_kw = {"executable_path": SANDBOX_CHROMIUM} if os.path.exists(SANDBOX_CHROMIUM) else {}
with sync_playwright() as p:
b = p.chromium.launch(**launch_kw)
pg = b.new_page()
pg.goto("file://" + os.path.abspath(SRC))
pg.wait_for_timeout(600)
pg.pdf(path=OUT, format="A4", print_background=True,
margin={"top": "11mm", "bottom": "12mm", "left": "10mm", "right": "10mm"})
b.close()
print("wrote", os.path.normpath(OUT))
plan_data.py
build/plan_data.py
# Zonqor — reconstructed base plan geometry.
#
# SOURCE: images/measurements/study_and_bedroom_revised_p1.jpg, cross-checked against
# images/measurements/kids_bedroom_only_p1.jpg (which carries a printed 1:100 scale bar).
#
# CALIBRATION — two independent routes, agreeing to 0.08%:
# 1. kids_bedroom_only_p1 scale bar: 10 m spans 599.0 px, 1 m ticks exactly 60.0 px,
# 0.1 m sub-ticks exactly 6.0 px -> 59.90 px/m
# 2. study_and_bedroom_revised_p1 is the same drawing plotted 1.3137x larger
# -> 78.68 px/m, which at 200 dpi is 9.99 mm per metre on paper = exactly 1:100
#
# ORIGIN: north-west outer corner of the building. x runs east, y runs south.
# All values in METRES, read off the drawing to the nearest ~0.02 m (1 px ~ 13 mm).
#
# ⚠ THIS IS A RECONSTRUCTION, NOT A SURVEY. Nobody has measured the flat. The drawing
# it is traced from is a designer's furniture-layout plot that itself carries the note
# "All dimensions to be verified on site prior to fabrication and installation."
# Confidence is recorded per room below. Do not order materials off these numbers.
SCALE_NOTE = "1:100, calibrated on a printed scale bar; ±3% overall, worse per room where noted"
# ---------------------------------------------------------------- building envelope
# Outer face of the external wall, clockwise from the NW corner.
# The west side steps in twice — two light wells that serve the bathroom, study,
# ensuite and entrance (photos confirm windows in exactly those rooms).
ENVELOPE = [
(0.00, 0.00), (7.21, 0.00), # north end
(7.21, 26.00), # east wall, full 26 m run
(0.00, 26.00), # south wall (front balcony beyond)
(0.00, 19.59), (2.43, 19.59), # -- light well 2 (entrance / ensuite)
(2.43, 13.50), (0.00, 13.50),
(0.00, 8.60), (2.19, 8.60), # -- light well 1 (bathroom / study)
(2.19, 4.60), (0.00, 4.60),
]
WALL_T = 0.22 # external wall thickness, measured consistently on all four sides
CEILING = 2.61 # dining area: 830 + 1050 + 730 mm off openplan_dimensions_1.jpg
# Front balcony: curved parapet (confirmed by balcony_plan_p1.jpg, which shows the BBQ
# and outdoor table set against a swept edge). Traced as an arc, not a measured curve.
BALCONY_ARC = dict(start=(6.68, 26.26), end=(1.90, 28.78), west_edge=(0.00, 28.78))
# ---------------------------------------------------------------- rooms
# code, name, x0, y0, x1, y1 (inner faces), confidence, note
# confidence: A = read off two sheets consistently, B = single reading, C = inferred
ROOMS = [
("BBL", "Back Balcony", 0.22, 0.19, 6.99, 1.04, "C",
"Full-width strip, only 0.85 m deep. Read as a balcony because tiles_sq_m.jpg excludes "
"it from the shaded 125 m². Could be a storage recess — walk it with Roderick."),
("CYB", "Bedroom — north-west", 0.22, 1.04, 3.05, 4.38, "B",
"Which of the two north rooms is Cy's and which is Rafe's is NOT established. "
"CYB is 'with window', RAF is 'with back balcony door' — resolve on site."),
("RAF", "Bedroom — north-east", 3.72, 1.04, 6.99, 4.38, "B",
"See note on the north-west bedroom. The 0.46 m strip between the two rooms is a "
"wardrobe/partition block, not habitable floor."),
("SCR", "Small Corridor", 2.44, 4.60, 5.48, 6.06, "C",
"LOWEST CONFIDENCE ROOM. A lobby between the kids' rooms and the bathroom. The "
"checklist's room order implies the small corridor sits nearer the main bedroom, "
"which does not match the geometry. Confirm what SCR-01 actually refers to."),
("BTH", "Main Bathroom", 2.44, 6.28, 5.26, 8.86, "A",
"The plain unfurnished rectangle on every sheet. West wall is external onto light "
"well 1 — matches the window in IMG_9873. Already finished in grey marble."),
("MCR", "Main Corridor", 5.48, 4.60, 6.86, 15.26, "A",
"Runs the full length of the east side, entrance to kids' rooms. Unobstructed on "
"the drawing for 10.66 m."),
("MBR", "Main Bedroom", 2.59, 9.07, 5.26, 13.28, "A",
"Study partition (OFF-01) forms its west wall at x=2.59."),
("OFF", "Study / Office", 0.22, 9.07, 2.59, 13.50, "B",
"Carved off the west side of the main bedroom by the new sound-proof gypsum "
"partition. Window onto light well 1."),
("ENS", "Ensuite", 3.19, 13.50, 5.26, 15.05, "B",
"Entered from the main bedroom; the 'wardrobe around ensuite door' callout sits "
"exactly here. Window onto light well 2 — matches IMG_9889. Already tiled."),
("ENT", "Entrance", 2.43, 15.26, 6.99, 17.29, "C",
"A threshold zone at the north end of the open plan rather than a room. Front door "
"and the existing consumer unit (IMG_9845) are on this stretch."),
("OPN", "Open Plan — living", 2.43, 17.29, 6.99, 19.59, "B",
"Sofa/TV end. Narrower because light well 2 cuts in on the west."),
("OP2", "Open Plan — dining & kitchen", 0.25, 19.81, 6.99, 26.00, "A",
"Full width. Fitted joinery runs along the east wall (0.38 m deep: TV unit, "
"shelving, enclosed storage)."),
]
# Rooms that photographs show are ALREADY tiled and finished, so are not part of any
# new 120x120 tiling area. See docs/07-site-observations.md.
ALREADY_FINISHED = {"BTH", "ENS", "FBL"}
# ---------------------------------------------------------------- what must be measured
TO_MEASURE = [
("Every room", "One diagonal per room", "Confirms the rooms are actually square. Nothing here is squared off."),
("BBL", "Depth and whether it is a balcony at all", "0.85 m is shallow; the room's purpose is unconfirmed."),
("CYB / RAF", "Which room is which", "Needed before any item priced per room can be signed off."),
("SCR", "Where the small corridor actually is", "Geometry and the checklist's room order disagree."),
("ENS", "Internal size and whether it is being re-tiled", "Drives OQ-05 and OQ-10."),
("FBL", "The curved parapet — chord and depth", "Traced as an arc; a curve cannot be scaled reliably."),
("All", "Floor-to-ceiling height in each room", "Only the dining area is known (2.61 m)."),
("All", "Door and window positions and widths", "Openings were not traced; they are needed for the electrical plan."),
]
rooms_data2.py
build/rooms_data2.py
# Zonqor refurbishment — rooms in Kurt's walk-through order (Aug 2026 revision).
# Order: Whole House (cross-room) then Front Balcony -> Open Plan -> Entrance ->
# Main Corridor -> Small Corridor -> Main Bedroom -> Study/Office -> Ensuite ->
# Main Bathroom -> Cy -> Rafe -> Back Balcony.
def item(title, group, tags="", notes="", dep_override=None, phase=None):
return {"title": title, "group": group, "tags": tags, "notes": notes,
"dep_override": dep_override, "phase": phase}
T_ELEC = '<span class="tag trade-elec">Electrical</span>'
T_GYP = '<span class="tag trade-gypsum">Gypsum</span>'
T_PLU = '<span class="tag trade-plumb">Plumbing</span>'
T_TIL = '<span class="tag trade-tile">Tiling</span>'
T_FIT = '<span class="tag trade-fit">Fittings</span>'
NEW = '<span class="tag status-new">NEW</span>'
KNOWN = '<span class="tag status-known">Known issue</span>'
REMOVE = '<span class="tag status-remove">Remove existing</span>'
TBC = '<span class="tag status-tbc">⚠ To confirm</span>'
def zone(z):
return f'<span class="tag room">{z}</span>'
ROOMS = []
# ------------------------------------------------------------------ WHOLE HOUSE
ROOMS.append(("Whole House", "WHS",
"Cross-room work that is not tied to a single room. Everything here is priced once, for the whole flat.", [
item("Strip out old parquet flooring throughout", "gypsum",
T_GYP + REMOVE + NEW,
"Lift and remove the existing parquet across the flat. First job on site — everything else waits on the floor being clear.",
dep_override="start", phase=1),
item("Check and level floor screed after parquet removal", "tile",
T_TIL + NEW,
"Once the parquet is up, check the screed for level and damage. Patch/level before tiling — 120x120 tiles are unforgiving on an uneven floor.",
dep_override="WHS-01 — parquet stripped", phase=4),
item("Skip hire and waste removal", "gypsum",
T_GYP + NEW,
"At least two skips: one after strip-out/demolition, one after first fix and chasing. Confirm who books and pays for the skip.",
dep_override="WHS-01 — parquet stripped", phase=3),
item("Fix electricity problems and points", "elec", T_ELEC,
"Diagnose and repair existing faults; check/replace faulty sockets and switches; add points as needed throughout the flat.",
dep_override="ENT-01 — new consumer unit", phase=2),
item("Lay 120x120 tiles — full house, including balconies (~145 sq m)", "tile", T_TIL,
"120x120 throughout the flat, ~145 sq m per Roderick's quote. ⚠ Site photos show the ensuite, main bathroom and front balcony are ALREADY TILED — re-measure the actual parquet area before this price is agreed (OQ-10).",
dep_override="WHS-04 — floor levelled; first-fix complete", phase=4),
]))
# --------------------------------------------------------------- FRONT BALCONY
ROOMS.append(("Front Balcony", "FBL",
"⚠ Outdoor kitchen design/equipment still to be specified (OQ-02) before ordering or starting those items.", [
item("2 side lights — BBQ side", "elec", T_ELEC + ' <span class="tag trade-elec">2 light points</span>', phase=2),
item("1 electrical/USB point — BBQ side (for camera)", "elec", T_ELEC + ' <span class="tag trade-elec">1 point</span>', phase=2),
item("Central floor lights (under railing) — mini lights or LED strip", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("Switch for balcony side lights", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>',
"At the current kitchen/balcony switch position.", phase=2),
item("Switch for balcony floor lights", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>',
"At the current kitchen/balcony switch position.", phase=2),
item("Rope lighting — front (outdoor)", "elec", T_ELEC,
dep_override="OQ-02 — outdoor kitchen spec", phase=2),
item("2 outdoor wall lights", "elec", T_ELEC + ' <span class="tag trade-elec">2 points</span>',
dep_override="OQ-02 — outdoor kitchen spec", phase=2),
item("2 waterproof plugs + LED kit for outdoor kitchen units", "elec", T_ELEC,
dep_override="OQ-02 — outdoor kitchen spec", phase=2),
item("Run gas line for outdoor kitchen", "plumb",
'<span class="tag trade-plumb">Gas/Plumbing</span>',
"Gas run must be pressure-tested and certified. Confirm bottle vs. mains supply and where the bottle housing sits.",
dep_override="OQ-02 — outdoor kitchen spec", phase=2),
item("Outdoor wall finish — PVC sheet OR tiles", "fit",
'<span class="tag trade-fit">Fittings/Tiling</span>' + TBC,
"One or the other, not both — still to be decided. See OQ-01.",
dep_override="OQ-01 — PVC vs. tiles; elec & gas rough-in (this room)", phase=9),
]))
# ------------------------------------------------------------------- OPEN PLAN
ROOMS.append(("Open Plan — Kitchen → Dining → Living", "OPN",
"One continuous space. Zone tags below show which end of the room each item sits in.", [
item("Dining Feature Wall + Fireplace Removal", "gypsum",
zone("Dining") + T_GYP + REMOVE,
"Remove the existing gypsum feature wall/partition and the fireplace to open the living/dining space. Confirm the final opening size before starting. Check for services in the wall before breaking.",
dep_override="start", phase=1),
item("Add gypsum behind sofa", "gypsum", zone("Living") + T_GYP,
"250mm wide x 1m high.",
dep_override="OPN-01 — feature wall removed", phase=5),
item("3 ceiling lights", "elec", zone("Kitchen") + T_ELEC + ' <span class="tag trade-elec">3 light points</span>',
"Reuses an existing electrical point.", phase=2),
item("Hood electrical point", "elec", zone("Kitchen") + T_ELEC + ' <span class="tag trade-elec">1 point</span>',
"Reuses an existing electrical point.", phase=2),
item("Fix oven electrical point — currently trips the salvavita (RCD)", "elec",
zone("Kitchen") + T_ELEC + ' <span class="tag trade-elec">2 points</span>' + KNOWN,
"Oven trips the salvavita when switched on. Check wiring, the connection point, and whether the oven needs its own dedicated circuit off the new unit.",
dep_override="ENT-01 — new consumer unit", phase=2),
item("Light points for dining cabinets", "elec", zone("Dining") + T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("Rope lighting within the new gypsum wall (behind sofa)", "elec",
zone("Living") + T_ELEC + ' <span class="tag trade-elec">1 light point</span>',
dep_override="OPN-02 — gypsum behind sofa", phase=2),
item("Switch for sofa rope lighting", "elec", zone("Living") + T_ELEC + ' <span class="tag trade-elec">1 switch</span>', phase=2),
item("2 plugs near sofa", "elec", zone("Living") + T_ELEC + ' <span class="tag trade-elec">2 points</span>', phase=2),
item("TV unit electrical point", "elec", zone("Living") + T_ELEC + TBC,
dep_override="OQ-07 — TV unit electrical spec", phase=2),
item("Ethernet point behind sofa", "elec", zone("Living") + T_ELEC,
"Run back to the central unit at the entrance — pull with the first-fix cabling, not after tiling.", phase=2),
item("Relocate double-pole switch when open-plan wall is removed", "elec", zone("Dining") + T_ELEC,
dep_override="OPN-01 — feature wall removed", phase=2),
item("Re-plumb kitchen sink onto main water feed (remove tank feed)", "plumb",
zone("Kitchen") + T_PLU + '<span class="tag">Prep for RO</span>',
"Switch the sink supply from the tank line to the main feed, and leave it ready for a future reverse-osmosis filter.",
dep_override="start", phase=2),
item("Install PVC sheet on wall with TV", "fit", zone("Living") + T_FIT + TBC,
dep_override="OQ-06 — PVC sheet spec; tiling complete", phase=9),
]))
# -------------------------------------------------------------------- ENTRANCE
ROOMS.append(("Entrance", "ENT",
"Home of the new electrical central unit — the first-fix hub for the whole flat. Everything electrical downstream waits on ENT-01.", [
item("Install new consumer unit / RCD (salvavita) panel", "elec",
T_ELEC + '<span class="tag status-known">Major item</span>',
"Full refresh of the electrical panel. Should also resolve the kitchen oven tripping the salvavita. Confirm final position and capacity (OQ-03) before ordering. Needs certification on completion.",
dep_override="OQ-03 — panel location and capacity", phase=2),
item("Intercom — install / replace unit and wiring", "elec", T_ELEC + NEW,
"Confirm whether the existing intercom is reused or replaced, and whether it is audio-only or video — this changes the cable pulled at first fix.",
dep_override="ENT-01 — new consumer unit", phase=2),
]))
# --------------------------------------------------------------- MAIN CORRIDOR
ROOMS.append(("Main Corridor", "MCR", None, [
item("Update rope lighting to match new dining rope light", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("New plug near feature wall", "elec", T_ELEC, phase=2),
item("Test existing wire feed", "elec", T_ELEC, phase=2),
item("Fix emergency light short circuit", "elec", T_ELEC + KNOWN,
"Trace and repair the short before the new unit is energised.", phase=2),
item("Install vent covers", "fit", T_FIT, phase=9),
item("Install corridor mirror — 3m x 1.5m", "fit", T_FIT,
"Large and heavy — confirm fixings and who lifts it. Fit after painting.", phase=9),
]))
# -------------------------------------------------------------- SMALL CORRIDOR
ROOMS.append(("Small Corridor", "SCR", None, [
item("Extend gypsum partition +10cm to fit new cabinets", "gypsum", T_GYP,
"Existing cabinet LED lighting point to be reused/kept.",
dep_override="start", phase=5),
]))
# ----------------------------------------------------------------- MAIN BEDROOM
ROOMS.append(("Main Bedroom", "MBR", None, [
item("Gypsum — close off old ceiling light area to create new layout", "gypsum", T_GYP, phase=5),
item("Remove 6 existing ceiling lights", "elec", T_ELEC + REMOVE, dep_override="start", phase=1),
item("Remove 2 bedside hanging lamps", "elec", T_ELEC + REMOVE, dep_override="start", phase=1),
item("Remove bedside window plugs", "elec", T_ELEC + REMOVE, dep_override="start", phase=1),
item("4 new ceiling lights above bed", "elec", T_ELEC + ' <span class="tag trade-elec">4 light points</span>', phase=2),
item("Rope lighting above bed", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("2 new bedside hanging lamps + 4 switches", "elec", T_ELEC + ' <span class="tag trade-elec">2 lights, 4 switches</span>',
"Switches positioned near bedside.", phase=2),
item("2 ceiling points near vanity/window", "elec", T_ELEC + ' <span class="tag trade-elec">2 light points</span>', phase=2),
item("Wire for wardrobe LED lighting (sensor-activated)", "elec", T_ELEC + ' <span class="tag trade-elec">1 light + sensor</span>',
"Sensor-operated, not a manual switch.", phase=2),
item("Cut into wardrobe for gyser/pump switches", "elec", T_ELEC,
"Gyser and pump switches built into the wardrobe carcass.",
dep_override="MBR-11 — wardrobe built (ULF)", phase=8),
item("Build wardrobe around ensuite door for maximum storage", "fit",
'<span class="tag trade-fit">Carpentry</span>',
dep_override="tiling and painting complete", phase=8),
item("Install vanity mirror", "fit", T_FIT + TBC,
"75cm from floor to vanity surface — fit after the vanity unit is in.",
dep_override="vanity unit installed (by others)", phase=9),
]))
# ---------------------------------------------------------------- STUDY/OFFICE
ROOMS.append(("Study / Office", "OFF",
"Sits off the Main Bedroom. ⚠ The new partition is a full wall, not an amendment — build it BEFORE tiling, not after (see the sequencing note in the plan).", [
item("Build new gypsum partition wall", "gypsum", T_GYP,
"2 layers of board on the inside, 75mm rockwool insulation in the middle, 1 single board on the outer side.",
dep_override="start", phase=5),
item("1 ceiling light point", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("Wire for LED lighting under desk", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("Plugs", "elec", T_ELEC + TBC,
dep_override="OQ-04 — plug quantity", phase=2),
item("Switch for ceiling light", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>', phase=2),
item("Switch for under-desk light", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>', phase=2),
item("Wire LED bar near door", "elec", T_ELEC + ' <span class="tag trade-elec">1 light point</span>', phase=2),
item("Internet/ethernet point near desk", "elec", T_ELEC,
"Run back to the central unit at the entrance during first fix.", phase=2),
item("Install office mirror — 2m x 2m", "fit", T_FIT, phase=9),
]))
# --------------------------------------------------------------------- ENSUITE
ROOMS.append(("Ensuite", "ENS", None, [
item("Paint ceiling", "gypsum", '<span class="tag trade-gypsum">Painting</span>',
dep_override="tiling complete", phase=6),
item("Add ensuite light switch", "elec", T_ELEC + ' <span class="tag trade-elec">1 switch</span>',
"Positioned via the main bedroom wardrobe cut-out.",
dep_override="MBR-10 — wardrobe cut-out", phase=8),
item("Tank/waterproof floor and shower walls before tiling", "tile",
T_TIL + NEW,
"Wet-area waterproofing was not in the original list but must happen before the ensuite is tiled — confirm it is in Roderick's price.",
dep_override="first fix complete", phase=4),
item("Lay ensuite tiles — 60x30 (dimensions to confirm)", "tile", T_TIL + TBC,
"⚠ Site photos show the ensuite is already tiled in large-format travertine. Confirm whether it is being re-tiled at all, or only the shower area (OQ-10).",
dep_override="OQ-05 & OQ-10 — confirm scope; ENS-03 waterproofing", phase=4),
item("Change shower glass + tray", "fit", T_FIT,
dep_override="tiling complete", phase=9),
]))
# --------------------------------------------------------------- MAIN BATHROOM
ROOMS.append(("Main Bathroom", "BTH", None, [
item("Install water heater (gyser) + electrical point", "plumb",
T_PLU + '<span class="tag trade-elec">1 point</span>',
"Confirm gyser size/position before first fix — it sets the pipe and cable runs.", phase=2),
item("Replace sink accessories with new", "fit", T_FIT, phase=9),
item("Replace flushing accessories with new", "fit", T_FIT, phase=9),
item("Attach mirror", "fit", T_FIT, phase=9),
item("Attach sink", "fit", T_FIT, phase=9),
item("Attach cabinets", "fit", T_FIT, phase=9),
]))
# ----------------------------------------------------------------- CY BEDROOM
ROOMS.append(("Cy's Bedroom (with window)", "CYB", None, [
item("Plugs under desk with USB", "elec", T_ELEC + ' <span class="tag trade-elec">1 point</span>', phase=2),
item("2 bedside plugs", "elec", T_ELEC + ' <span class="tag trade-elec">2 points</span>', phase=2),
item("Wire for wardrobe LED lighting (sensor-activated)", "elec", T_ELEC + ' <span class="tag trade-elec">1 light + sensor</span>', phase=2),
item("Wire feed for LED under wall furniture", "elec", T_ELEC, phase=2),
item("Internet point near desk", "elec", T_ELEC, phase=2),
]))
# --------------------------------------------------------------- RAFE BEDROOM
ROOMS.append(("Rafe's Bedroom (with back balcony door)", "RAF", None, [
item("Plugs under desk with USB", "elec", T_ELEC + ' <span class="tag trade-elec">1 point</span>', phase=2),
item("2 bedside plugs", "elec", T_ELEC + ' <span class="tag trade-elec">2 points</span>', phase=2),
item("Wire for wardrobe LED lighting (sensor-activated)", "elec", T_ELEC + ' <span class="tag trade-elec">1 light + sensor</span>', phase=2),
item("Wire feed for LED under wall furniture", "elec", T_ELEC, phase=2),
item("Internet point near desk", "elec", T_ELEC, phase=2),
]))
# ---------------------------------------------------------------- BACK BALCONY
ROOMS.append(("Back Balcony", "BBL",
"Off Rafe's bedroom. No items listed yet beyond the whole-house tiling — walk it with Roderick on day one and write in anything found.", [
item("Back balcony — confirm scope on site", "gypsum",
'<span class="tag status-tbc">⚠ Scope to confirm</span>' + NEW,
"Covered by the whole-house 120x120 tiling (WHS-05). Check for: outdoor lighting, a weatherproof socket, drainage falls, railing condition and door threshold. Add items here as found.",
dep_override="start", phase=1),
]))
# ------------------------------------------------------------------- pictures
ROOM_IMAGES = {
"WHS": [("tiles_sq_m.jpg", "Floor plan — tiling area reference (125 sqm marked; 145 sqm quoted)")],
"FBL": [("balcony_1.jpg", "Front balcony / outdoor kitchen — render 1"),
("balcony_2.jpg", "Front balcony / outdoor kitchen — render 2"),
('site_9826.jpg', 'SITE: front balcony — BBQ corner, existing feed & camera'),
('site_9827.jpg', 'SITE: front balcony looking out — adhesive stacked'),
('site_9828.jpg', 'SITE: front balcony — already tiled, membrane at edge'),
('site_9895.jpg', 'SITE: sliding door to front balcony'),
],
"OPN": [("openplan_dimensions_1.jpg", "Open plan — measurement drawing 1"),
("openplan_dimensions_2.jpg", "Open plan — measurement drawing 2"),
("openplan_dimensions_3.jpg", "Open plan — measurement drawing 3"),
("1__dining__feature_wall_revised.jpg", "Dining — feature wall render"),
("7__dining_+_kitchen_revised.jpg", "Kitchen / dining — render"),
("15_dining_+_tv_unit_revised.jpg", "Dining — TV unit render"),
("2__sofa_revised.jpg", "Living — sofa wall render"),
('site_9834.jpg', 'SITE: open plan overview — parquet still down, 120 tiles delivered'),
('site_9835.jpg', 'SITE: kitchen island, units wrapped'),
('site_9839.jpg', 'SITE: dining feature wall + fireplace, wall opened below'),
('site_9893.jpg', 'SITE: fireplace close-up — installed, wall broken beneath'),
('site_9840.jpg', 'SITE: living — existing socket run'),
('site_9896.jpg', 'SITE: gypsum board delivered on site'),
],
"ENT": [
('site_9845.jpg', 'SITE: existing consumer unit, thermostat & entrance door'),
],
"MCR": [("10_corridor_revised.jpg", "Main corridor — render"),
("corridor_dimensions_1.jpg", "Corridor — measurement drawing"),
('site_9849.jpg', 'SITE: main corridor looking through'),
('site_9850.jpg', 'SITE: main corridor — existing cove lighting'),
('site_9843.jpg', 'SITE: corridor switch position'),
],
"SCR": [("12_corridor_small_revised.jpg", "Small corridor — render")],
"MBR": [("main_bedroom_1.jpg", "Main bedroom — render (overview)"),
("main_bedroom_6-bed.jpg", "Main bedroom — bed wall render"),
("main_bedroom_4-_wardrobe.jpg", "Main bedroom — wardrobe render"),
("main_bedroom_5-_vanity.jpg", "Main bedroom — vanity render"),
("main_bedroom_dimensions_1.jpg", "Main bedroom — measurement drawing 1"),
("main_bedroom_dimensions_2.jpg", "Main bedroom — measurement drawing 2")],
"OFF": [("study_1.jpg", "Study / office — render 1"),
("study_2.jpg", "Study / office — render 2"),
("study_3.jpg", "Study / office — render 3")],
"ENS": [
('site_9886.jpg', 'SITE: ensuite — existing shower enclosure'),
('site_9888.jpg', 'SITE: ensuite — shower tray & glass to be changed'),
('site_9889.jpg', 'SITE: ensuite — WC & window'),
('site_9890.jpg', 'SITE: ensuite — overview'),
],
"BTH": [
('site_9872.jpg', 'SITE: main bathroom — existing finish'),
('site_9873.jpg', 'SITE: main bathroom — WC & window'),
('site_9875.jpg', 'SITE: main bathroom — ceiling & downlights'),
],
"CYB": [("Cy_kids_bedroom_7.jpg", "Cy's bedroom — render 1"),
("Cy_kids_bedroom_10.jpg", "Cy's bedroom — render 2"),
("kids_bedroom_dimensions_2.jpg", "Kids bedroom — measurement drawing")],
"RAF": [("Rafe_kids_bedroom_1.jpg", "Rafe's bedroom — render 1"),
("Rafe_kids_bedroom_5.jpg", "Rafe's bedroom — render 2"),
("kids_bedroom_dimensions_1.jpg", "Kids bedroom — measurement drawing")],
"BBL": [],
}
# ------------------------------------------------------------------- schedule
# Phases follow Roderick's proposed order, with the gaps filled in (6-11).
PHASES = {
1: ("Strip-out & demolition", "Parquet up, feature wall and fireplace down, old fittings removed."),
2: ("First fix — plumbing, electrical, ethernet", "Consumer unit, chasing, all cable and pipe runs, gas line. Nothing gets closed up until this is signed off."),
3: ("Clear site — skip", "Rubble and chasing debris out before tiling starts."),
4: ("Tiling", "Screed levelled, wet areas tanked, 120x120 laid through the flat and balconies, ensuite in 60x30."),
5: ("Gypsum — partition & amendments", "Study partition, main bedroom ceiling closure, sofa wall, small corridor extension."),
6: ("Painting", "Ceilings and walls, including the ensuite ceiling."),
7: ("Second fix electrical", "Fit and terminate every point, hang light fittings, LED strips and sensors, then test and certify."),
8: ("Carpentry — ULF", "Wardrobes, cabinets, TV unit. Roderick returns to cut in the gyser/pump and ensuite switches once the wardrobe is up."),
9: ("Fittings, sanitary & outdoor", "Mirrors, shower glass and tray, sanitary ware, vent covers, PVC/tile outdoor wall finish."),
10: ("Snagging & handover", "Walk the flat against this checklist, list defects, fix, final clean, hand over certificates."),
}
verify_refs.py
build/verify_refs.py
"""Validate the generated checklist: no broken ID cross-references, no duplicate IDs.
MUST pass after every rebuild — item IDs are positional and reordering silently breaks refs."""
import re, sys, os
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from rooms_data2 import ROOMS
from bs4 import BeautifulSoup
html_path = os.path.join(HERE, "..", "output", "renovation_checklist.html")
soup = BeautifulSoup(open(html_path).read(), "html.parser")
real = set(e.get_text(strip=True) for e in soup.select(".wi-id"))
appendix = set(e.find("b").get_text(strip=True) for e in soup.select(".ap figcaption") if e.find("b"))
oq = {i for i in real if i.startswith("OQ-")}
print(f"item IDs: {len(real)-len(oq)} OQ: {len(oq)} appendix: {len(appendix)}")
text = soup.get_text(" ")
codes = {c for _n, c, _b, _i in ROOMS}
tokens = set(re.findall(r"\b(?:" + "|".join(codes) + r"|OQ)-\d{2}\b", text))
atokens = set(re.findall(r"\bA\d{2}\b", text))
bad = sorted(t for t in tokens if t not in real)
bada = sorted(t for t in atokens if t not in appendix)
ids = [e.get_text(strip=True) for e in soup.select(".wi-id")]
dupes = sorted({i for i in ids if ids.count(i) > 1})
items = soup.select(".wi")
nophase = [i.select_one(".wi-id").get_text(strip=True) for i in items if not i.select_one(".phase-chip")]
print("BROKEN item refs :", bad or "none")
print("BROKEN picture refs:", bada or "none")
print("DUPLICATE IDs :", dupes or "none")
print("missing phase chip:", nophase or "none")
print(f"price inputs: {len(soup.select('.price'))} work items: {len(items)}")
sys.exit(1 if (bad or bada or dupes) else 0)