Vibecode Wanderfugl
track this build4 phases, 12 steps, beginner friendly0%Trip planning is one of the most one-shottable categories there is: it is days, places, times, notes and a map, which is a CRUD app with a calendar view. An agent can give you a local itinerary builder with day-by-day cards, confirmation numbers, an ICS export and a pinned map in a single sitting, and it will feel better than a spreadsheet. The gap is everything that touches live travel data: flight and hotel prices, schedule changes, timezone-correct arrival math, place search with real coordinates, and inbox parsing of booking emails. If the paid product's value is mostly structure and a nice UI, self-hosting wins; if it is live data feeds or a polished phone app you use offline in an airport, it does not. Rate it kinda until you know which of those you are actually paying for.
You are building a lean indie version of Wanderfugl. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Wanderfugl · indie build A travel planning tool whose defining requirement is that it works when you most need it, which is in a foreign airport with no data connection, and whose hardest correctness problem is that every time in a trip belongs to a different place. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a local-first store and optional sync | the moment you need this most is the moment you have no connection | | Time | Every item stored as an instant with the timezone of its location | a flight departs in local time at the airport it leaves from | | Documents | Confirmations stored as files on the device | a booking reference in a cloud you cannot reach is not a booking reference | | Import | Confirmation parsing with manual entry as an equal path | parsers break on the format that matters and you are already travelling | | Sharing | Read-only trip links that also work offline once opened | the person meeting you needs the details too | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local-first application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A current timezone database** · free - Why: Zone rules change by government decision and a stale database puts a flight an hour wrong, which is the failure that costs you the flight. - Get it: Your platform's zone data with a plan for keeping it current, plus a library handling ambiguous local times explicitly. - Verify: A daylight transition produces the correct duration for a flight spanning it - [ ] **A real multi-country trip's confirmations** · free - Why: Timezone handling, connection checking and parsing all break on real bookings, and a single-country trip exercises almost none of it. - Get it: Use a real trip with at least three countries, an overnight flight and a connection under two hours. - Verify: A real trip with three zones and a tight connection is entered - [ ] **A device you can genuinely take offline** · free - Why: The central claim is that it works with no connection, and testing that with a network available proves nothing. - Get it: Any device where you can disable networking entirely, ideally the one you would actually travel with. - Verify: Every screen is exercised with networking disabled - [ ] **Real confirmation documents in several formats** · free - Why: Parsing breaks on the format that matters, and the mixture of email, attachment and screenshot is what real bookings actually look like. - Get it: Collect confirmations from several airlines and hotels, including one that is only a screenshot. - Verify: At least six confirmations exist across three formats ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. - Live flight, train and hotel pricing or availability - Automatic booking email parsing into your itinerary - A real mobile app with offline access while travelling - Timezone and schedule-change handling done correctly by someone else - Sharing a polished read-only itinerary with travel companions If one of those is essential to you, that is the reason to keep paying for Wanderfugl, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Wanderfugl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a trip planner that works at the airport with no signal to replace Wanderfugl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a local-first store and optional sync · the moment you need this most is the moment you have no connection - Time: Every item stored as an instant with the timezone of its location · a flight departs in local time at the airport it leaves from - Documents: Confirmations stored as files on the device · a booking reference in a cloud you cannot reach is not a booking reference - Import: Confirmation parsing with manual entry as an equal path · parsers break on the format that matters and you are already travelling - Sharing: Read-only trip links that also work offline once opened · the person meeting you needs the details too ### Data model (create this before Phase 1) - trips(id, title, starts_on, ends_on, base_currency) - items(id, trip_id, kind, title, starts_at_utc, ends_at_utc, start_place_id, end_place_id, confirmation_ref, notes) - places(id, name, timezone, country, coordinates, address) · the timezone belongs to the place - documents(id, item_id, path, kind, added_at, offline_available) · stored on the device - costs(item_id, amount_minor, currency, rate_to_base, paid_by, at) - conflicts(trip_id, kind, item_ids_json, detail, detected_at) · overlaps and impossible connections - An item stores its start and end place separately, because a flight's departure and arrival are in different timezones and treating them as one is the classic error. ### Phase 1 · Times that mean the right place Build: Store every item as instants with the timezone of the place each end belongs to. Display in the relevant local time with the zone stated. Handle an overnight flight crossing a date line and a journey crossing a daylight transition. Trap: Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. Done when: A flight's departure shows in the departure city's local time and its arrival in the arrival city's, both labelled. An overnight flight crossing the date line shows the correct arrival date. An item spanning a daylight transition computes the correct duration. Changing the device's timezone does not change any displayed local time. Do not build yet: No offline handling, documents or import. ### Phase 2 · Everything available with no connection Build: Store all trip data and attached documents on the device. Verify the entire application functions with networking disabled. Handle a document too large to store and say so rather than silently omitting it. Trap: Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. Done when: The complete trip including all documents opens with networking disabled, verified for every screen. A document that could not be stored is reported rather than silently missing. Storage use is visible per trip. A trip added on one device is fully available offline after a single sync. Do not build yet: No import, costs or sharing yet. ### Phase 3 · Getting bookings in without retyping Build: Parse confirmation emails and files into items where the format allows, with manual entry as an equally supported path. Report a parse that failed rather than importing something wrong. Keep the original document attached. Trap: A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. Done when: A confirmation parses into an item with its times confirmed by the user before saving. A parse failure falls back to manual entry with the document still attached. Manual entry of a flight takes under a minute, timed by hand. A partially parsed confirmation fills what it can and asks for the rest. Do not build yet: No conflicts, costs or sharing yet. ### Phase 4 · Catching the impossible connection Build: Detect overlapping items, connections with insufficient time between arrival and next departure, and a stay whose dates do not cover the nights you are there. Compute across timezones correctly. Trap: Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. Done when: A connection with insufficient time is flagged, computed across timezones as instants. Overlapping items are detected. A stay not covering a night you are in that city is flagged. The minimum connection time assumption is stated rather than presented as fact. Do not build yet: No costs or sharing yet. ### Phase 5 · Costs and sharing the plan Build: Record costs in their own currency with the rate at the time, converting for a total. Share a read-only trip that works offline once opened. Export the itinerary as a document and a calendar file. Trap: Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. Done when: Costs are stored in their own currency with the rate at the time and the total does not move when rates change. A shared trip link works offline after being opened once. Export produces both a readable itinerary and a calendar file with correct times per zone. Sharing does not expose documents unless explicitly included. Do not build yet: Nothing after this. ### Out of scope (and why) - Booking anything, which is a different business with inventory, payments and cancellation terms. - Live flight status and disruption handling, which needs a data subscription and is where the real value of a travel app sits. - Recommendations and destination content, which is a publishing operation rather than a planning tool. - Group trip coordination with shared editing, which is a synchronisation problem on top of everything here. ### README must contain - That every place carries its own timezone and an item's two ends are resolved separately - That the entire application including documents works with networking disabled, and this is tested - That parsed booking times are confirmed before saving, because a silently wrong time is found at the airport - That connection times are compared as instants rather than displayed times - That costs keep the rate at the time of payment so the total does not move ===== AGENTS.md ===== # Agent instructions · Wanderfugl indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with a local-first store and optional sync, Every item stored as an instant with the timezone of its location, Confirmations stored as files on the device, Confirmation parsing with manual entry as an equal path, Read-only trip links that also work offline once opened. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ===== BUILD_PLAN.md ===== # Build plan · Wanderfugl A travel planning tool whose defining requirement is that it works when you most need it, which is in a foreign airport with no data connection, and whose hardest correctness problem is that every time in a trip belongs to a different place. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Times that mean the right place A flight shows its departure in the departure city's time. ### Steps 1. Attach a timezone to the place Not to the trip. The trip spans several. Files: `src/places.ts` 2. Resolve both ends separately A flight departs and arrives in different zones. It is the classic error. Files: `src/items.ts`, `src/items.test.ts` 3. Always state the zone A bare time on a trip across zones is an ambiguity. Files: `src/display.ts` ### Done when - [ ] Departure and arrival show in their own local times - [ ] A date line crossing shows the correct arrival date - [ ] Daylight transitions produce correct durations - [ ] Changing the device timezone changes nothing displayed ### Watch out - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. ## Phase 2 · Everything available with no connection The whole trip including documents opens with the network off. ### Steps 1. Store documents on the device A reference behind a login you cannot reach is not a reference. Files: `src/documents.ts` 2. Test every screen offline Assuming it works offline is how it does not. Files: `tests/offline.test.ts` 3. Report what could not be stored Silently missing is the worst version of missing. Files: `src/documents.ts` ### Done when - [ ] The whole trip opens with networking disabled - [ ] Unstored documents are reported - [ ] Storage use is visible per trip - [ ] One sync makes a trip fully offline available ### Watch out - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. ## Phase 3 · Getting bookings in without retyping A confirmation email becomes an itinerary item, or manual entry is just as fast. ### Steps 1. Require confirmation of parsed times A silently wrong departure time is discovered at the airport. Files: `src/import.ts` 2. Make manual entry fast It is the path that always works and will be used often. Files: `src/manual.ts` 3. Keep the original document It is the actual evidence of the booking. Files: `src/documents.ts` ### Done when - [ ] Parsed times are confirmed before saving - [ ] A parse failure falls back to manual entry - [ ] Manual flight entry takes under a minute - [ ] Partial parses ask for the remainder ### Watch out - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. ## Phase 4 · Catching the impossible connection A plan that cannot physically work is flagged before you travel. ### Steps 1. Compare instants, never displayed times Across zones the displayed comparison is meaningless. Files: `src/conflicts.ts`, `src/conflicts.test.ts` 2. Check accommodation covers the nights The gap nobody notices until they arrive. Files: `src/conflicts.ts` 3. State your assumptions A minimum connection time is a guess and should read as one. Files: `src/conflicts.ts` ### Done when - [ ] Insufficient connections are flagged across zones - [ ] Overlapping items are detected - [ ] Uncovered nights are flagged - [ ] Assumptions are stated rather than implied ### Watch out - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ## Not in this build - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. ## After v1, if you want it - A day view that shows only what is next, which is what you want while travelling - Currency spending totals per country, which is the question after the trip - Travel document expiry warnings, since a passport rule is the most expensive thing to miss ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Trips and itineraries, stored on the device. DB_PATH=~/.local/share/trips/trips.db # Required. Confirmation files, stored locally so they work with no connection. DOCUMENTS_DIR=~/.local/share/trips/documents # Required. An assumption, stated as one rather than presented as authoritative. MIN_CONNECTION_MINUTES=60 # Required. Present as a constant. The moment you need this is the moment you have no signal. OFFLINE_REQUIRED=always # Required. For totals. Individual costs keep their own currency and rate. BASE_CURRENCY=EUR # Optional. Optional. Absent means one device, which works completely. SYNC_REMOTE=s3://my-trips/store
You are building a lean indie version of Wanderfugl. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Wanderfugl · indie build A travel planning tool whose defining requirement is that it works when you most need it, which is in a foreign airport with no data connection, and whose hardest correctness problem is that every time in a trip belongs to a different place. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a local-first store and optional sync | the moment you need this most is the moment you have no connection | | Time | Every item stored as an instant with the timezone of its location | a flight departs in local time at the airport it leaves from | | Documents | Confirmations stored as files on the device | a booking reference in a cloud you cannot reach is not a booking reference | | Import | Confirmation parsing with manual entry as an equal path | parsers break on the format that matters and you are already travelling | | Sharing | Read-only trip links that also work offline once opened | the person meeting you needs the details too | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local-first application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A current timezone database** · free - Why: Zone rules change by government decision and a stale database puts a flight an hour wrong, which is the failure that costs you the flight. - Get it: Your platform's zone data with a plan for keeping it current, plus a library handling ambiguous local times explicitly. - Verify: A daylight transition produces the correct duration for a flight spanning it - [ ] **A real multi-country trip's confirmations** · free - Why: Timezone handling, connection checking and parsing all break on real bookings, and a single-country trip exercises almost none of it. - Get it: Use a real trip with at least three countries, an overnight flight and a connection under two hours. - Verify: A real trip with three zones and a tight connection is entered - [ ] **A device you can genuinely take offline** · free - Why: The central claim is that it works with no connection, and testing that with a network available proves nothing. - Get it: Any device where you can disable networking entirely, ideally the one you would actually travel with. - Verify: Every screen is exercised with networking disabled - [ ] **Real confirmation documents in several formats** · free - Why: Parsing breaks on the format that matters, and the mixture of email, attachment and screenshot is what real bookings actually look like. - Get it: Collect confirmations from several airlines and hotels, including one that is only a screenshot. - Verify: At least six confirmations exist across three formats ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. - Live flight, train and hotel pricing or availability - Automatic booking email parsing into your itinerary - A real mobile app with offline access while travelling - Timezone and schedule-change handling done correctly by someone else - Sharing a polished read-only itinerary with travel companions If one of those is essential to you, that is the reason to keep paying for Wanderfugl, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Wanderfugl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a trip planner that works at the airport with no signal to replace Wanderfugl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a local-first store and optional sync · the moment you need this most is the moment you have no connection - Time: Every item stored as an instant with the timezone of its location · a flight departs in local time at the airport it leaves from - Documents: Confirmations stored as files on the device · a booking reference in a cloud you cannot reach is not a booking reference - Import: Confirmation parsing with manual entry as an equal path · parsers break on the format that matters and you are already travelling - Sharing: Read-only trip links that also work offline once opened · the person meeting you needs the details too ### Data model (create this before Phase 1) - trips(id, title, starts_on, ends_on, base_currency) - items(id, trip_id, kind, title, starts_at_utc, ends_at_utc, start_place_id, end_place_id, confirmation_ref, notes) - places(id, name, timezone, country, coordinates, address) · the timezone belongs to the place - documents(id, item_id, path, kind, added_at, offline_available) · stored on the device - costs(item_id, amount_minor, currency, rate_to_base, paid_by, at) - conflicts(trip_id, kind, item_ids_json, detail, detected_at) · overlaps and impossible connections - An item stores its start and end place separately, because a flight's departure and arrival are in different timezones and treating them as one is the classic error. ### Phase 1 · Times that mean the right place Build: Store every item as instants with the timezone of the place each end belongs to. Display in the relevant local time with the zone stated. Handle an overnight flight crossing a date line and a journey crossing a daylight transition. Trap: Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. Done when: A flight's departure shows in the departure city's local time and its arrival in the arrival city's, both labelled. An overnight flight crossing the date line shows the correct arrival date. An item spanning a daylight transition computes the correct duration. Changing the device's timezone does not change any displayed local time. Do not build yet: No offline handling, documents or import. ### Phase 2 · Everything available with no connection Build: Store all trip data and attached documents on the device. Verify the entire application functions with networking disabled. Handle a document too large to store and say so rather than silently omitting it. Trap: Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. Done when: The complete trip including all documents opens with networking disabled, verified for every screen. A document that could not be stored is reported rather than silently missing. Storage use is visible per trip. A trip added on one device is fully available offline after a single sync. Do not build yet: No import, costs or sharing yet. ### Phase 3 · Getting bookings in without retyping Build: Parse confirmation emails and files into items where the format allows, with manual entry as an equally supported path. Report a parse that failed rather than importing something wrong. Keep the original document attached. Trap: A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. Done when: A confirmation parses into an item with its times confirmed by the user before saving. A parse failure falls back to manual entry with the document still attached. Manual entry of a flight takes under a minute, timed by hand. A partially parsed confirmation fills what it can and asks for the rest. Do not build yet: No conflicts, costs or sharing yet. ### Phase 4 · Catching the impossible connection Build: Detect overlapping items, connections with insufficient time between arrival and next departure, and a stay whose dates do not cover the nights you are there. Compute across timezones correctly. Trap: Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. Done when: A connection with insufficient time is flagged, computed across timezones as instants. Overlapping items are detected. A stay not covering a night you are in that city is flagged. The minimum connection time assumption is stated rather than presented as fact. Do not build yet: No costs or sharing yet. ### Phase 5 · Costs and sharing the plan Build: Record costs in their own currency with the rate at the time, converting for a total. Share a read-only trip that works offline once opened. Export the itinerary as a document and a calendar file. Trap: Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. Done when: Costs are stored in their own currency with the rate at the time and the total does not move when rates change. A shared trip link works offline after being opened once. Export produces both a readable itinerary and a calendar file with correct times per zone. Sharing does not expose documents unless explicitly included. Do not build yet: Nothing after this. ### Out of scope (and why) - Booking anything, which is a different business with inventory, payments and cancellation terms. - Live flight status and disruption handling, which needs a data subscription and is where the real value of a travel app sits. - Recommendations and destination content, which is a publishing operation rather than a planning tool. - Group trip coordination with shared editing, which is a synchronisation problem on top of everything here. ### README must contain - That every place carries its own timezone and an item's two ends are resolved separately - That the entire application including documents works with networking disabled, and this is tested - That parsed booking times are confirmed before saving, because a silently wrong time is found at the airport - That connection times are compared as instants rather than displayed times - That costs keep the rate at the time of payment so the total does not move ===== AGENTS.md ===== # Agent instructions · Wanderfugl indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with a local-first store and optional sync, Every item stored as an instant with the timezone of its location, Confirmations stored as files on the device, Confirmation parsing with manual entry as an equal path, Read-only trip links that also work offline once opened. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ===== BUILD_PLAN.md ===== # Build plan · Wanderfugl A travel planning tool whose defining requirement is that it works when you most need it, which is in a foreign airport with no data connection, and whose hardest correctness problem is that every time in a trip belongs to a different place. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Times that mean the right place A flight shows its departure in the departure city's time. ### Steps 1. Attach a timezone to the place Not to the trip. The trip spans several. Files: `src/places.ts` 2. Resolve both ends separately A flight departs and arrives in different zones. It is the classic error. Files: `src/items.ts`, `src/items.test.ts` 3. Always state the zone A bare time on a trip across zones is an ambiguity. Files: `src/display.ts` ### Done when - [ ] Departure and arrival show in their own local times - [ ] A date line crossing shows the correct arrival date - [ ] Daylight transitions produce correct durations - [ ] Changing the device timezone changes nothing displayed ### Watch out - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. ## Phase 2 · Everything available with no connection The whole trip including documents opens with the network off. ### Steps 1. Store documents on the device A reference behind a login you cannot reach is not a reference. Files: `src/documents.ts` 2. Test every screen offline Assuming it works offline is how it does not. Files: `tests/offline.test.ts` 3. Report what could not be stored Silently missing is the worst version of missing. Files: `src/documents.ts` ### Done when - [ ] The whole trip opens with networking disabled - [ ] Unstored documents are reported - [ ] Storage use is visible per trip - [ ] One sync makes a trip fully offline available ### Watch out - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. ## Phase 3 · Getting bookings in without retyping A confirmation email becomes an itinerary item, or manual entry is just as fast. ### Steps 1. Require confirmation of parsed times A silently wrong departure time is discovered at the airport. Files: `src/import.ts` 2. Make manual entry fast It is the path that always works and will be used often. Files: `src/manual.ts` 3. Keep the original document It is the actual evidence of the booking. Files: `src/documents.ts` ### Done when - [ ] Parsed times are confirmed before saving - [ ] A parse failure falls back to manual entry - [ ] Manual flight entry takes under a minute - [ ] Partial parses ask for the remainder ### Watch out - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. ## Phase 4 · Catching the impossible connection A plan that cannot physically work is flagged before you travel. ### Steps 1. Compare instants, never displayed times Across zones the displayed comparison is meaningless. Files: `src/conflicts.ts`, `src/conflicts.test.ts` 2. Check accommodation covers the nights The gap nobody notices until they arrive. Files: `src/conflicts.ts` 3. State your assumptions A minimum connection time is a guess and should read as one. Files: `src/conflicts.ts` ### Done when - [ ] Insufficient connections are flagged across zones - [ ] Overlapping items are detected - [ ] Uncovered nights are flagged - [ ] Assumptions are stated rather than implied ### Watch out - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ## Not in this build - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. ## After v1, if you want it - A day view that shows only what is next, which is what you want while travelling - Currency spending totals per country, which is the question after the trip - Travel document expiry warnings, since a passport rule is the most expensive thing to miss ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Trips and itineraries, stored on the device. DB_PATH=~/.local/share/trips/trips.db # Required. Confirmation files, stored locally so they work with no connection. DOCUMENTS_DIR=~/.local/share/trips/documents # Required. An assumption, stated as one rather than presented as authoritative. MIN_CONNECTION_MINUTES=60 # Required. Present as a constant. The moment you need this is the moment you have no signal. OFFLINE_REQUIRED=always # Required. For totals. Individual costs keep their own currency and rate. BASE_CURRENCY=EUR # Optional. Optional. Absent means one device, which works completely. SYNC_REMOTE=s3://my-trips/store
You are building a production product version of Wanderfugl. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Wanderfugl · product brief ## Problem Trip planning is one of the most one-shottable categories there is: it is days, places, times, notes and a map, which is a CRUD app with a calendar view. An agent can give you a local itinerary builder with day-by-day cards, confirmation numbers, an ICS export and a pinned map in a single sitting, and it will feel better than a spreadsheet. The gap is everything that touches live travel data: flight and hotel prices, schedule changes, timezone-correct arrival math, place search with real coordinates, and inbox parsing of booking emails. If the paid product's value is mostly structure and a nice UI, self-hosting wins; if it is live data feeds or a polished phone app you use offline in an airport, it does not. Rate it kinda until you know which of those you are actually paying for. ## Product outcome A trip planner that opens at an airport with no signal, shows every time in the right city's clock, and tells you before you leave that your connection does not work. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node and a browser - A maps tile key if you want a real map instead of a static list - Optional: a place search API key for coordinates ## Explicit non-goals for v1 - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. - Live flight, train and hotel pricing or availability - Automatic booking email parsing into your itinerary - A real mobile app with offline access while travelling - Timezone and schedule-change handling done correctly by someone else - Sharing a polished read-only itinerary with travel companions ## Success criteria - Departure and arrival are confirmed to show in their own local times - The whole application is confirmed to work with networking disabled - Parsed times are confirmed to require confirmation - Connection checks are confirmed to use instants - Costs are confirmed not to move when rates change ===== BRIEF.md ===== # Build brief · Wanderfugl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a trip planner that works at the airport with no signal to replace Wanderfugl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a local-first store and optional sync · the moment you need this most is the moment you have no connection - Time: Every item stored as an instant with the timezone of its location · a flight departs in local time at the airport it leaves from - Documents: Confirmations stored as files on the device · a booking reference in a cloud you cannot reach is not a booking reference - Import: Confirmation parsing with manual entry as an equal path · parsers break on the format that matters and you are already travelling - Sharing: Read-only trip links that also work offline once opened · the person meeting you needs the details too ### Data model (create this before Phase 1) - trips(id, title, starts_on, ends_on, base_currency) - items(id, trip_id, kind, title, starts_at_utc, ends_at_utc, start_place_id, end_place_id, confirmation_ref, notes) - places(id, name, timezone, country, coordinates, address) · the timezone belongs to the place - documents(id, item_id, path, kind, added_at, offline_available) · stored on the device - costs(item_id, amount_minor, currency, rate_to_base, paid_by, at) - conflicts(trip_id, kind, item_ids_json, detail, detected_at) · overlaps and impossible connections - An item stores its start and end place separately, because a flight's departure and arrival are in different timezones and treating them as one is the classic error. ### Phase 1 · Times that mean the right place Build: Store every item as instants with the timezone of the place each end belongs to. Display in the relevant local time with the zone stated. Handle an overnight flight crossing a date line and a journey crossing a daylight transition. Trap: Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. Done when: A flight's departure shows in the departure city's local time and its arrival in the arrival city's, both labelled. An overnight flight crossing the date line shows the correct arrival date. An item spanning a daylight transition computes the correct duration. Changing the device's timezone does not change any displayed local time. Do not build yet: No offline handling, documents or import. ### Phase 2 · Everything available with no connection Build: Store all trip data and attached documents on the device. Verify the entire application functions with networking disabled. Handle a document too large to store and say so rather than silently omitting it. Trap: Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. Done when: The complete trip including all documents opens with networking disabled, verified for every screen. A document that could not be stored is reported rather than silently missing. Storage use is visible per trip. A trip added on one device is fully available offline after a single sync. Do not build yet: No import, costs or sharing yet. ### Phase 3 · Getting bookings in without retyping Build: Parse confirmation emails and files into items where the format allows, with manual entry as an equally supported path. Report a parse that failed rather than importing something wrong. Keep the original document attached. Trap: A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. Done when: A confirmation parses into an item with its times confirmed by the user before saving. A parse failure falls back to manual entry with the document still attached. Manual entry of a flight takes under a minute, timed by hand. A partially parsed confirmation fills what it can and asks for the rest. Do not build yet: No conflicts, costs or sharing yet. ### Phase 4 · Catching the impossible connection Build: Detect overlapping items, connections with insufficient time between arrival and next departure, and a stay whose dates do not cover the nights you are there. Compute across timezones correctly. Trap: Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. Done when: A connection with insufficient time is flagged, computed across timezones as instants. Overlapping items are detected. A stay not covering a night you are in that city is flagged. The minimum connection time assumption is stated rather than presented as fact. Do not build yet: No costs or sharing yet. ### Phase 5 · Costs and sharing the plan Build: Record costs in their own currency with the rate at the time, converting for a total. Share a read-only trip that works offline once opened. Export the itinerary as a document and a calendar file. Trap: Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. Done when: Costs are stored in their own currency with the rate at the time and the total does not move when rates change. A shared trip link works offline after being opened once. Export produces both a readable itinerary and a calendar file with correct times per zone. Sharing does not expose documents unless explicitly included. Do not build yet: Nothing after this. ### Out of scope (and why) - Booking anything, which is a different business with inventory, payments and cancellation terms. - Live flight status and disruption handling, which needs a data subscription and is where the real value of a travel app sits. - Recommendations and destination content, which is a publishing operation rather than a planning tool. - Group trip coordination with shared editing, which is a synchronisation problem on top of everything here. ### README must contain - That every place carries its own timezone and an item's two ends are resolved separately - That the entire application including documents works with networking disabled, and this is tested - That parsed booking times are confirmed before saving, because a silently wrong time is found at the airport - That connection times are compared as instants rather than displayed times - That costs keep the rate at the time of payment so the total does not move ===== ARCHITECTURE.md ===== # Architecture · Wanderfugl ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a local-first store and optional sync | the moment you need this most is the moment you have no connection | | Time | Every item stored as an instant with the timezone of its location | a flight departs in local time at the airport it leaves from | | Documents | Confirmations stored as files on the device | a booking reference in a cloud you cannot reach is not a booking reference | | Import | Confirmation parsing with manual entry as an equal path | parsers break on the format that matters and you are already travelling | | Sharing | Read-only trip links that also work offline once opened | the person meeting you needs the details too | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Time | Per-place zones and per-end resolution. | Any library, provided both ends of an item resolve separately. | | Offline | Local storage of data and documents. | Not swappable for cloud references, which fail exactly when needed. | | Import | Conservative parsing with confirmation. | Any parser, provided failures fall back rather than guessing. | | Conflicts | Instant-based feasibility checks. | Any implementation, provided comparisons use instants. | | Sharing | Read-only trips that survive going offline. | Any transport, provided a shared trip works without a connection. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Trips and itineraries, stored on the device. - `DOCUMENTS_DIR` · required · Confirmation files, stored locally so they work with no connection. - `MIN_CONNECTION_MINUTES` · required · An assumption, stated as one rather than presented as authoritative. - `OFFLINE_REQUIRED` · required · Present as a constant. The moment you need this is the moment you have no signal. - `BASE_CURRENCY` · required · For totals. Individual costs keep their own currency and rate. - `SYNC_REMOTE` · optional · Optional. Absent means one device, which works completely. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Wanderfugl product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with a local-first store and optional sync, Every item stored as an instant with the timezone of its location, Confirmations stored as files on the device, Confirmation parsing with manual entry as an equal path, Read-only trip links that also work offline once opened. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. - Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. ===== MILESTONES.md ===== # Delivery milestones · Wanderfugl Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Times that mean the right place A flight shows its departure in the departure city's time. ### Steps 1. Attach a timezone to the place Not to the trip. The trip spans several. Files: `src/places.ts` 2. Resolve both ends separately A flight departs and arrives in different zones. It is the classic error. Files: `src/items.ts`, `src/items.test.ts` 3. Always state the zone A bare time on a trip across zones is an ambiguity. Files: `src/display.ts` ### Done when - [ ] Departure and arrival show in their own local times - [ ] A date line crossing shows the correct arrival date - [ ] Daylight transitions produce correct durations - [ ] Changing the device timezone changes nothing displayed ### Watch out - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. ## M2 · Everything available with no connection The whole trip including documents opens with the network off. ### Steps 1. Store documents on the device A reference behind a login you cannot reach is not a reference. Files: `src/documents.ts` 2. Test every screen offline Assuming it works offline is how it does not. Files: `tests/offline.test.ts` 3. Report what could not be stored Silently missing is the worst version of missing. Files: `src/documents.ts` ### Done when - [ ] The whole trip opens with networking disabled - [ ] Unstored documents are reported - [ ] Storage use is visible per trip - [ ] One sync makes a trip fully offline available ### Watch out - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. ## M3 · Getting bookings in without retyping A confirmation email becomes an itinerary item, or manual entry is just as fast. ### Steps 1. Require confirmation of parsed times A silently wrong departure time is discovered at the airport. Files: `src/import.ts` 2. Make manual entry fast It is the path that always works and will be used often. Files: `src/manual.ts` 3. Keep the original document It is the actual evidence of the booking. Files: `src/documents.ts` ### Done when - [ ] Parsed times are confirmed before saving - [ ] A parse failure falls back to manual entry - [ ] Manual flight entry takes under a minute - [ ] Partial parses ask for the remainder ### Watch out - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. ## M4 · Catching the impossible connection A plan that cannot physically work is flagged before you travel. ### Steps 1. Compare instants, never displayed times Across zones the displayed comparison is meaningless. Files: `src/conflicts.ts`, `src/conflicts.test.ts` 2. Check accommodation covers the nights The gap nobody notices until they arrive. Files: `src/conflicts.ts` 3. State your assumptions A minimum connection time is a guess and should read as one. Files: `src/conflicts.ts` ### Done when - [ ] Insufficient connections are flagged across zones - [ ] Overlapping items are detected - [ ] Uncovered nights are flagged - [ ] Assumptions are stated rather than implied ### Watch out - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ## M5 · Costs and sharing the plan (production only) You know what the trip cost and the person meeting you has the details. ### Steps 1. Fix the rate at the time of payment A total that changes weekly is not a total. Files: `src/costs.ts` 2. Make a shared trip work offline The person meeting you is also travelling. Files: `src/share.ts` 3. Export times per zone correctly A calendar file with wrong times is worse than none. Files: `src/export.ts` ### Done when - [ ] Costs do not move when rates change - [ ] A shared trip works offline once opened - [ ] Calendar export has correct per-zone times - [ ] Sharing excludes documents by default ### Watch out - Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. ===== OPERATIONS.md ===== # Operations · Wanderfugl ## Backup A trip's documents are the evidence of bookings and are needed at borders. Back them up before travelling rather than relying on the device, and remember the backup must be reachable offline too. ## Restore Restore and confirm documents are present rather than only referenced, since a restore that returns metadata without the files leaves you with an itinerary and no confirmations. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool, the check is the offline test suite. It is the only claim that matters and it silently breaks whenever a new feature reaches for a network call. ## Incident checklist Being unable to open a booking reference at a desk is the failure this exists to prevent, and it always means something was stored as a reference rather than a file. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Departure and arrival are confirmed to show in their own local times - [ ] The whole application is confirmed to work with networking disabled - [ ] Parsed times are confirmed to require confirmation - [ ] Connection checks are confirmed to use instants - [ ] Costs are confirmed not to move when rates change ## Launch constraint Do not market omitted Wanderfugl capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Trips and itineraries, stored on the device. DB_PATH=~/.local/share/trips/trips.db # Required. Confirmation files, stored locally so they work with no connection. DOCUMENTS_DIR=~/.local/share/trips/documents # Required. An assumption, stated as one rather than presented as authoritative. MIN_CONNECTION_MINUTES=60 # Required. Present as a constant. The moment you need this is the moment you have no signal. OFFLINE_REQUIRED=always # Required. For totals. Individual costs keep their own currency and rate. BASE_CURRENCY=EUR # Optional. Optional. Absent means one device, which works completely. SYNC_REMOTE=s3://my-trips/store
# Wanderfugl · indie build A travel planning tool whose defining requirement is that it works when you most need it, which is in a foreign airport with no data connection, and whose hardest correctness problem is that every time in a trip belongs to a different place. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a local-first store and optional sync | the moment you need this most is the moment you have no connection | | Time | Every item stored as an instant with the timezone of its location | a flight departs in local time at the airport it leaves from | | Documents | Confirmations stored as files on the device | a booking reference in a cloud you cannot reach is not a booking reference | | Import | Confirmation parsing with manual entry as an equal path | parsers break on the format that matters and you are already travelling | | Sharing | Read-only trip links that also work offline once opened | the person meeting you needs the details too | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local-first application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A current timezone database** · free - Why: Zone rules change by government decision and a stale database puts a flight an hour wrong, which is the failure that costs you the flight. - Get it: Your platform's zone data with a plan for keeping it current, plus a library handling ambiguous local times explicitly. - Verify: A daylight transition produces the correct duration for a flight spanning it - [ ] **A real multi-country trip's confirmations** · free - Why: Timezone handling, connection checking and parsing all break on real bookings, and a single-country trip exercises almost none of it. - Get it: Use a real trip with at least three countries, an overnight flight and a connection under two hours. - Verify: A real trip with three zones and a tight connection is entered - [ ] **A device you can genuinely take offline** · free - Why: The central claim is that it works with no connection, and testing that with a network available proves nothing. - Get it: Any device where you can disable networking entirely, ideally the one you would actually travel with. - Verify: Every screen is exercised with networking disabled - [ ] **Real confirmation documents in several formats** · free - Why: Parsing breaks on the format that matters, and the mixture of email, attachment and screenshot is what real bookings actually look like. - Get it: Collect confirmations from several airlines and hotels, including one that is only a screenshot. - Verify: At least six confirmations exist across three formats ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. - Live flight, train and hotel pricing or availability - Automatic booking email parsing into your itinerary - A real mobile app with offline access while travelling - Timezone and schedule-change handling done correctly by someone else - Sharing a polished read-only itinerary with travel companions If one of those is essential to you, that is the reason to keep paying for Wanderfugl, and the README should say so rather than pretend.
# Build brief · Wanderfugl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a trip planner that works at the airport with no signal to replace Wanderfugl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a local-first store and optional sync · the moment you need this most is the moment you have no connection - Time: Every item stored as an instant with the timezone of its location · a flight departs in local time at the airport it leaves from - Documents: Confirmations stored as files on the device · a booking reference in a cloud you cannot reach is not a booking reference - Import: Confirmation parsing with manual entry as an equal path · parsers break on the format that matters and you are already travelling - Sharing: Read-only trip links that also work offline once opened · the person meeting you needs the details too ### Data model (create this before Phase 1) - trips(id, title, starts_on, ends_on, base_currency) - items(id, trip_id, kind, title, starts_at_utc, ends_at_utc, start_place_id, end_place_id, confirmation_ref, notes) - places(id, name, timezone, country, coordinates, address) · the timezone belongs to the place - documents(id, item_id, path, kind, added_at, offline_available) · stored on the device - costs(item_id, amount_minor, currency, rate_to_base, paid_by, at) - conflicts(trip_id, kind, item_ids_json, detail, detected_at) · overlaps and impossible connections - An item stores its start and end place separately, because a flight's departure and arrival are in different timezones and treating them as one is the classic error. ### Phase 1 · Times that mean the right place Build: Store every item as instants with the timezone of the place each end belongs to. Display in the relevant local time with the zone stated. Handle an overnight flight crossing a date line and a journey crossing a daylight transition. Trap: Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. Done when: A flight's departure shows in the departure city's local time and its arrival in the arrival city's, both labelled. An overnight flight crossing the date line shows the correct arrival date. An item spanning a daylight transition computes the correct duration. Changing the device's timezone does not change any displayed local time. Do not build yet: No offline handling, documents or import. ### Phase 2 · Everything available with no connection Build: Store all trip data and attached documents on the device. Verify the entire application functions with networking disabled. Handle a document too large to store and say so rather than silently omitting it. Trap: Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. Done when: The complete trip including all documents opens with networking disabled, verified for every screen. A document that could not be stored is reported rather than silently missing. Storage use is visible per trip. A trip added on one device is fully available offline after a single sync. Do not build yet: No import, costs or sharing yet. ### Phase 3 · Getting bookings in without retyping Build: Parse confirmation emails and files into items where the format allows, with manual entry as an equally supported path. Report a parse that failed rather than importing something wrong. Keep the original document attached. Trap: A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. Done when: A confirmation parses into an item with its times confirmed by the user before saving. A parse failure falls back to manual entry with the document still attached. Manual entry of a flight takes under a minute, timed by hand. A partially parsed confirmation fills what it can and asks for the rest. Do not build yet: No conflicts, costs or sharing yet. ### Phase 4 · Catching the impossible connection Build: Detect overlapping items, connections with insufficient time between arrival and next departure, and a stay whose dates do not cover the nights you are there. Compute across timezones correctly. Trap: Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. Done when: A connection with insufficient time is flagged, computed across timezones as instants. Overlapping items are detected. A stay not covering a night you are in that city is flagged. The minimum connection time assumption is stated rather than presented as fact. Do not build yet: No costs or sharing yet. ### Phase 5 · Costs and sharing the plan Build: Record costs in their own currency with the rate at the time, converting for a total. Share a read-only trip that works offline once opened. Export the itinerary as a document and a calendar file. Trap: Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. Done when: Costs are stored in their own currency with the rate at the time and the total does not move when rates change. A shared trip link works offline after being opened once. Export produces both a readable itinerary and a calendar file with correct times per zone. Sharing does not expose documents unless explicitly included. Do not build yet: Nothing after this. ### Out of scope (and why) - Booking anything, which is a different business with inventory, payments and cancellation terms. - Live flight status and disruption handling, which needs a data subscription and is where the real value of a travel app sits. - Recommendations and destination content, which is a publishing operation rather than a planning tool. - Group trip coordination with shared editing, which is a synchronisation problem on top of everything here. ### README must contain - That every place carries its own timezone and an item's two ends are resolved separately - That the entire application including documents works with networking disabled, and this is tested - That parsed booking times are confirmed before saving, because a silently wrong time is found at the airport - That connection times are compared as instants rather than displayed times - That costs keep the rate at the time of payment so the total does not move
# Agent instructions · Wanderfugl indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with a local-first store and optional sync, Every item stored as an instant with the timezone of its location, Confirmations stored as files on the device, Confirmation parsing with manual entry as an equal path, Read-only trip links that also work offline once opened. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative.
# Build plan · Wanderfugl A travel planning tool whose defining requirement is that it works when you most need it, which is in a foreign airport with no data connection, and whose hardest correctness problem is that every time in a trip belongs to a different place. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Times that mean the right place A flight shows its departure in the departure city's time. ### Steps 1. Attach a timezone to the place Not to the trip. The trip spans several. Files: `src/places.ts` 2. Resolve both ends separately A flight departs and arrives in different zones. It is the classic error. Files: `src/items.ts`, `src/items.test.ts` 3. Always state the zone A bare time on a trip across zones is an ambiguity. Files: `src/display.ts` ### Done when - [ ] Departure and arrival show in their own local times - [ ] A date line crossing shows the correct arrival date - [ ] Daylight transitions produce correct durations - [ ] Changing the device timezone changes nothing displayed ### Watch out - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. ## Phase 2 · Everything available with no connection The whole trip including documents opens with the network off. ### Steps 1. Store documents on the device A reference behind a login you cannot reach is not a reference. Files: `src/documents.ts` 2. Test every screen offline Assuming it works offline is how it does not. Files: `tests/offline.test.ts` 3. Report what could not be stored Silently missing is the worst version of missing. Files: `src/documents.ts` ### Done when - [ ] The whole trip opens with networking disabled - [ ] Unstored documents are reported - [ ] Storage use is visible per trip - [ ] One sync makes a trip fully offline available ### Watch out - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. ## Phase 3 · Getting bookings in without retyping A confirmation email becomes an itinerary item, or manual entry is just as fast. ### Steps 1. Require confirmation of parsed times A silently wrong departure time is discovered at the airport. Files: `src/import.ts` 2. Make manual entry fast It is the path that always works and will be used often. Files: `src/manual.ts` 3. Keep the original document It is the actual evidence of the booking. Files: `src/documents.ts` ### Done when - [ ] Parsed times are confirmed before saving - [ ] A parse failure falls back to manual entry - [ ] Manual flight entry takes under a minute - [ ] Partial parses ask for the remainder ### Watch out - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. ## Phase 4 · Catching the impossible connection A plan that cannot physically work is flagged before you travel. ### Steps 1. Compare instants, never displayed times Across zones the displayed comparison is meaningless. Files: `src/conflicts.ts`, `src/conflicts.test.ts` 2. Check accommodation covers the nights The gap nobody notices until they arrive. Files: `src/conflicts.ts` 3. State your assumptions A minimum connection time is a guess and should read as one. Files: `src/conflicts.ts` ### Done when - [ ] Insufficient connections are flagged across zones - [ ] Overlapping items are detected - [ ] Uncovered nights are flagged - [ ] Assumptions are stated rather than implied ### Watch out - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ## Not in this build - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. ## After v1, if you want it - A day view that shows only what is next, which is what you want while travelling - Currency spending totals per country, which is the question after the trip - Travel document expiry warnings, since a passport rule is the most expensive thing to miss
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Trips and itineraries, stored on the device. DB_PATH=~/.local/share/trips/trips.db # Required. Confirmation files, stored locally so they work with no connection. DOCUMENTS_DIR=~/.local/share/trips/documents # Required. An assumption, stated as one rather than presented as authoritative. MIN_CONNECTION_MINUTES=60 # Required. Present as a constant. The moment you need this is the moment you have no signal. OFFLINE_REQUIRED=always # Required. For totals. Individual costs keep their own currency and rate. BASE_CURRENCY=EUR # Optional. Optional. Absent means one device, which works completely. SYNC_REMOTE=s3://my-trips/store
# Wanderfugl · product brief ## Problem Trip planning is one of the most one-shottable categories there is: it is days, places, times, notes and a map, which is a CRUD app with a calendar view. An agent can give you a local itinerary builder with day-by-day cards, confirmation numbers, an ICS export and a pinned map in a single sitting, and it will feel better than a spreadsheet. The gap is everything that touches live travel data: flight and hotel prices, schedule changes, timezone-correct arrival math, place search with real coordinates, and inbox parsing of booking emails. If the paid product's value is mostly structure and a nice UI, self-hosting wins; if it is live data feeds or a polished phone app you use offline in an airport, it does not. Rate it kinda until you know which of those you are actually paying for. ## Product outcome A trip planner that opens at an airport with no signal, shows every time in the right city's clock, and tells you before you leave that your connection does not work. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node and a browser - A maps tile key if you want a real map instead of a static list - Optional: a place search API key for coordinates ## Explicit non-goals for v1 - Booking anything. - Live flight status and disruption handling. - Destination recommendations and content. - Group trips with shared editing. - Live flight, train and hotel pricing or availability - Automatic booking email parsing into your itinerary - A real mobile app with offline access while travelling - Timezone and schedule-change handling done correctly by someone else - Sharing a polished read-only itinerary with travel companions ## Success criteria - Departure and arrival are confirmed to show in their own local times - The whole application is confirmed to work with networking disabled - Parsed times are confirmed to require confirmation - Connection checks are confirmed to use instants - Costs are confirmed not to move when rates change
# Build brief · Wanderfugl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a trip planner that works at the airport with no signal to replace Wanderfugl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a local-first store and optional sync · the moment you need this most is the moment you have no connection - Time: Every item stored as an instant with the timezone of its location · a flight departs in local time at the airport it leaves from - Documents: Confirmations stored as files on the device · a booking reference in a cloud you cannot reach is not a booking reference - Import: Confirmation parsing with manual entry as an equal path · parsers break on the format that matters and you are already travelling - Sharing: Read-only trip links that also work offline once opened · the person meeting you needs the details too ### Data model (create this before Phase 1) - trips(id, title, starts_on, ends_on, base_currency) - items(id, trip_id, kind, title, starts_at_utc, ends_at_utc, start_place_id, end_place_id, confirmation_ref, notes) - places(id, name, timezone, country, coordinates, address) · the timezone belongs to the place - documents(id, item_id, path, kind, added_at, offline_available) · stored on the device - costs(item_id, amount_minor, currency, rate_to_base, paid_by, at) - conflicts(trip_id, kind, item_ids_json, detail, detected_at) · overlaps and impossible connections - An item stores its start and end place separately, because a flight's departure and arrival are in different timezones and treating them as one is the classic error. ### Phase 1 · Times that mean the right place Build: Store every item as instants with the timezone of the place each end belongs to. Display in the relevant local time with the zone stated. Handle an overnight flight crossing a date line and a journey crossing a daylight transition. Trap: Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. Done when: A flight's departure shows in the departure city's local time and its arrival in the arrival city's, both labelled. An overnight flight crossing the date line shows the correct arrival date. An item spanning a daylight transition computes the correct duration. Changing the device's timezone does not change any displayed local time. Do not build yet: No offline handling, documents or import. ### Phase 2 · Everything available with no connection Build: Store all trip data and attached documents on the device. Verify the entire application functions with networking disabled. Handle a document too large to store and say so rather than silently omitting it. Trap: Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. Done when: The complete trip including all documents opens with networking disabled, verified for every screen. A document that could not be stored is reported rather than silently missing. Storage use is visible per trip. A trip added on one device is fully available offline after a single sync. Do not build yet: No import, costs or sharing yet. ### Phase 3 · Getting bookings in without retyping Build: Parse confirmation emails and files into items where the format allows, with manual entry as an equally supported path. Report a parse that failed rather than importing something wrong. Keep the original document attached. Trap: A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. Done when: A confirmation parses into an item with its times confirmed by the user before saving. A parse failure falls back to manual entry with the document still attached. Manual entry of a flight takes under a minute, timed by hand. A partially parsed confirmation fills what it can and asks for the rest. Do not build yet: No conflicts, costs or sharing yet. ### Phase 4 · Catching the impossible connection Build: Detect overlapping items, connections with insufficient time between arrival and next departure, and a stay whose dates do not cover the nights you are there. Compute across timezones correctly. Trap: Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. Done when: A connection with insufficient time is flagged, computed across timezones as instants. Overlapping items are detected. A stay not covering a night you are in that city is flagged. The minimum connection time assumption is stated rather than presented as fact. Do not build yet: No costs or sharing yet. ### Phase 5 · Costs and sharing the plan Build: Record costs in their own currency with the rate at the time, converting for a total. Share a read-only trip that works offline once opened. Export the itinerary as a document and a calendar file. Trap: Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert. Done when: Costs are stored in their own currency with the rate at the time and the total does not move when rates change. A shared trip link works offline after being opened once. Export produces both a readable itinerary and a calendar file with correct times per zone. Sharing does not expose documents unless explicitly included. Do not build yet: Nothing after this. ### Out of scope (and why) - Booking anything, which is a different business with inventory, payments and cancellation terms. - Live flight status and disruption handling, which needs a data subscription and is where the real value of a travel app sits. - Recommendations and destination content, which is a publishing operation rather than a planning tool. - Group trip coordination with shared editing, which is a synchronisation problem on top of everything here. ### README must contain - That every place carries its own timezone and an item's two ends are resolved separately - That the entire application including documents works with networking disabled, and this is tested - That parsed booking times are confirmed before saving, because a silently wrong time is found at the airport - That connection times are compared as instants rather than displayed times - That costs keep the rate at the time of payment so the total does not move
# Architecture · Wanderfugl ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a local-first store and optional sync | the moment you need this most is the moment you have no connection | | Time | Every item stored as an instant with the timezone of its location | a flight departs in local time at the airport it leaves from | | Documents | Confirmations stored as files on the device | a booking reference in a cloud you cannot reach is not a booking reference | | Import | Confirmation parsing with manual entry as an equal path | parsers break on the format that matters and you are already travelling | | Sharing | Read-only trip links that also work offline once opened | the person meeting you needs the details too | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Time | Per-place zones and per-end resolution. | Any library, provided both ends of an item resolve separately. | | Offline | Local storage of data and documents. | Not swappable for cloud references, which fail exactly when needed. | | Import | Conservative parsing with confirmation. | Any parser, provided failures fall back rather than guessing. | | Conflicts | Instant-based feasibility checks. | Any implementation, provided comparisons use instants. | | Sharing | Read-only trips that survive going offline. | Any transport, provided a shared trip works without a connection. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Trips and itineraries, stored on the device. - `DOCUMENTS_DIR` · required · Confirmation files, stored locally so they work with no connection. - `MIN_CONNECTION_MINUTES` · required · An assumption, stated as one rather than presented as authoritative. - `OFFLINE_REQUIRED` · required · Present as a constant. The moment you need this is the moment you have no signal. - `BASE_CURRENCY` · required · For totals. Individual costs keep their own currency and rate. - `SYNC_REMOTE` · optional · Optional. Absent means one device, which works completely. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Wanderfugl product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with a local-first store and optional sync, Every item stored as an instant with the timezone of its location, Confirmations stored as files on the device, Confirmation parsing with manual entry as an equal path, Read-only trip links that also work offline once opened. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. - Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert.
# Delivery milestones · Wanderfugl Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Times that mean the right place A flight shows its departure in the departure city's time. ### Steps 1. Attach a timezone to the place Not to the trip. The trip spans several. Files: `src/places.ts` 2. Resolve both ends separately A flight departs and arrives in different zones. It is the classic error. Files: `src/items.ts`, `src/items.test.ts` 3. Always state the zone A bare time on a trip across zones is an ambiguity. Files: `src/display.ts` ### Done when - [ ] Departure and arrival show in their own local times - [ ] A date line crossing shows the correct arrival date - [ ] Daylight transitions produce correct durations - [ ] Changing the device timezone changes nothing displayed ### Watch out - Storing a trip in one timezone means a flight leaving at seven in the morning shows as three in the morning once you have travelled, which is exactly when you are relying on it. Every place carries its own zone and every item's two ends are resolved separately. ## M2 · Everything available with no connection The whole trip including documents opens with the network off. ### Steps 1. Store documents on the device A reference behind a login you cannot reach is not a reference. Files: `src/documents.ts` 2. Test every screen offline Assuming it works offline is how it does not. Files: `tests/offline.test.ts` 3. Report what could not be stored Silently missing is the worst version of missing. Files: `src/documents.ts` ### Done when - [ ] The whole trip opens with networking disabled - [ ] Unstored documents are reported - [ ] Storage use is visible per trip - [ ] One sync makes a trip fully offline available ### Watch out - Storing confirmation documents as links to a cloud service means that at the immigration desk, with no roaming and no airport network, the booking reference you need is behind a login you cannot reach. Download and store the files, and check that the whole application works with networking disabled rather than assuming it does. ## M3 · Getting bookings in without retyping A confirmation email becomes an itinerary item, or manual entry is just as fast. ### Steps 1. Require confirmation of parsed times A silently wrong departure time is discovered at the airport. Files: `src/import.ts` 2. Make manual entry fast It is the path that always works and will be used often. Files: `src/manual.ts` 3. Keep the original document It is the actual evidence of the booking. Files: `src/documents.ts` ### Done when - [ ] Parsed times are confirmed before saving - [ ] A parse failure falls back to manual entry - [ ] Manual flight entry takes under a minute - [ ] Partial parses ask for the remainder ### Watch out - A parser that silently produces a wrong departure time is far worse than one that fails, because you will act on it and only find out at the airport. Parse conservatively, require confirmation of the extracted times, and fall back to manual entry rather than guessing. ## M4 · Catching the impossible connection A plan that cannot physically work is flagged before you travel. ### Steps 1. Compare instants, never displayed times Across zones the displayed comparison is meaningless. Files: `src/conflicts.ts`, `src/conflicts.test.ts` 2. Check accommodation covers the nights The gap nobody notices until they arrive. Files: `src/conflicts.ts` 3. State your assumptions A minimum connection time is a guess and should read as one. Files: `src/conflicts.ts` ### Done when - [ ] Insufficient connections are flagged across zones - [ ] Overlapping items are detected - [ ] Uncovered nights are flagged - [ ] Assumptions are stated rather than implied ### Watch out - Comparing a landing time and a departure time as displayed strings, rather than as instants, makes a forty minute connection look like an eight hour one when the two are in different zones. Compare instants, and state the minimum connection assumption you are using rather than implying it is authoritative. ## M5 · Costs and sharing the plan (production only) You know what the trip cost and the person meeting you has the details. ### Steps 1. Fix the rate at the time of payment A total that changes weekly is not a total. Files: `src/costs.ts` 2. Make a shared trip work offline The person meeting you is also travelling. Files: `src/share.ts` 3. Export times per zone correctly A calendar file with wrong times is worse than none. Files: `src/export.ts` ### Done when - [ ] Costs do not move when rates change - [ ] A shared trip works offline once opened - [ ] Calendar export has correct per-zone times - [ ] Sharing excludes documents by default ### Watch out - Converting costs at today's rate rather than the rate when paid means the trip total changes every time you open it, and the figure you tell someone is different from the one you told them last week. Store the rate with the cost and never re-convert.
# Operations · Wanderfugl ## Backup A trip's documents are the evidence of bookings and are needed at borders. Back them up before travelling rather than relying on the device, and remember the backup must be reachable offline too. ## Restore Restore and confirm documents are present rather than only referenced, since a restore that returns metadata without the files leaves you with an itinerary and no confirmations. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool, the check is the offline test suite. It is the only claim that matters and it silently breaks whenever a new feature reaches for a network call. ## Incident checklist Being unable to open a booking reference at a desk is the failure this exists to prevent, and it always means something was stored as a reference rather than a file. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Departure and arrival are confirmed to show in their own local times - [ ] The whole application is confirmed to work with networking disabled - [ ] Parsed times are confirmed to require confirmation - [ ] Connection checks are confirmed to use instants - [ ] Costs are confirmed not to move when rates change ## Launch constraint Do not market omitted Wanderfugl capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Trips and itineraries, stored on the device. DB_PATH=~/.local/share/trips/trips.db # Required. Confirmation files, stored locally so they work with no connection. DOCUMENTS_DIR=~/.local/share/trips/documents # Required. An assumption, stated as one rather than presented as authoritative. MIN_CONNECTION_MINUTES=60 # Required. Present as a constant. The moment you need this is the moment you have no signal. OFFLINE_REQUIRED=always # Required. For totals. Individual costs keep their own currency and rate. BASE_CURRENCY=EUR # Optional. Optional. Absent means one device, which works completely. SYNC_REMOTE=s3://my-trips/store
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because the hard part of trip planning is not storing the plan, it is getting the plan in and out without typing. Forwarding a confirmation email and having flights, seats and hotel nights appear correctly, with the right timezones and the right dates, is worth money to people who travel often. Add a phone app that works in an airport with no data and a share link your travel partner can actually read, and a self-hosted version starts to feel like homework right when you least want homework.
xLive flight, train and hotel pricing or availability
xAutomatic booking email parsing into your itinerary
xA real mobile app with offline access while travelling
xTimezone and schedule-change handling done correctly by someone else
xSharing a polished read-only itinerary with travel companions
Nothing worth pointing at. That's why the prompt exists.
Vibecode Wanderfugl
Kinda. The core of Wanderfugl is buildable in a weekend with the prompt on this page, but there are real gaps: Live flight, train and hotel pricing or availability, Automatic booking email parsing into your itinerary. Read the honest list above before committing.
How much does Wanderfugl cost?
Wanderfugl costs about $9/month (Plus, checked 2026-08-18), which is $108 per year.
What do I lose by replacing Wanderfugl?
Honestly: Live flight, train and hotel pricing or availability; Automatic booking email parsing into your itinerary; A real mobile app with offline access while travelling; Timezone and schedule-change handling done correctly by someone else; Sharing a polished read-only itinerary with travel companions. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Wanderfugl?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.