Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
preview-link:
runs-on: ubuntu-latest
steps:
- name: Post preview link
- name: Post preview links
uses: actions/github-script@v7
with:
script: |
Expand All @@ -21,8 +21,8 @@ jobs:
const repo = context.repo.repo;
const pr = context.payload.pull_request;
const sha = pr.head.sha;
const preview = `https://rawcdn.githack.com/${owner}/${repo}/${sha}/index.html`;
const body = `${marker}\n## 🌍 Web preview\n\n[Open this PR as a live website](${preview})\n\nPreview is pinned to commit \`${sha.slice(0, 7)}\` and updates automatically when the PR changes.`;
const root = `https://rawcdn.githack.com/${owner}/${repo}/${sha}`;
const body = `${marker}\n## 🌍 Web preview\n\n- [Open travel dashboard](${root}/index.html)\n- [Open live flight prices](${root}/flights.html)\n\nPreview is pinned to commit \`${sha.slice(0, 7)}\` and updates automatically when the PR changes.`;

const comments = await github.paginate(github.rest.issues.listComments, {
owner,
Expand All @@ -36,17 +36,7 @@ jobs:
);

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body,
});
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}
147 changes: 147 additions & 0 deletions .github/workflows/update-flight-prices.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: Update live flight prices

on:
workflow_dispatch:
schedule:
# 07:17 in Vietnam (UTC+7). One refresh uses 4 SerpApi searches.
# Daily schedule keeps the project comfortably inside the 250-search free tier.
- cron: '17 0 * * *'

permissions:
contents: write
pages: write
issues: write

concurrency:
group: live-flight-prices-${{ github.ref }}
cancel-in-progress: false

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Check SerpApi secret
id: config
env:
SERPAPI_API_KEY: ${{ secrets.SERPAPI_API_KEY }}
run: |
if [ -z "$SERPAPI_API_KEY" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::SERPAPI_API_KEY is not configured. Add it in Settings → Secrets and variables → Actions."
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi

- name: Fetch Google Flights prices via SerpApi
if: steps.config.outputs.enabled == 'true'
env:
SERPAPI_API_KEY: ${{ secrets.SERPAPI_API_KEY }}
run: node scripts/fetch-flights.mjs

- name: Evaluate price alert
if: steps.config.outputs.enabled == 'true' && github.ref_name == 'main'
env:
GH_TOKEN: ${{ github.token }}
ALERT_AMOUNT: ${{ vars.FLIGHT_ALERT_AMOUNT }}
ALERT_CURRENCY: ${{ vars.FLIGHT_ALERT_CURRENCY }}
shell: bash
run: |
if [ -z "$ALERT_AMOUNT" ]; then
echo "No FLIGHT_ALERT_AMOUNT repository variable configured; skipping GitHub Issue alert."
exit 0
fi

CURRENT_AMOUNT=$(node -p "require('./data/flights.json').cheapest?.offer?.total_amount || ''")
CURRENT_CURRENCY=$(node -p "require('./data/flights.json').cheapest?.offer?.total_currency || ''")
CURRENT_AIRLINE=$(node -p "require('./data/flights.json').cheapest?.offer?.owner?.name || 'Unknown airline'")
CURRENT_SCENARIO=$(node -p "require('./data/flights.json').cheapest?.label || 'China trip'")
CHECKED_AT=$(node -p "require('./data/flights.json').generated_at || new Date().toISOString()")
ALERT_CURRENCY=${ALERT_CURRENCY:-$CURRENT_CURRENCY}

if [ -z "$CURRENT_AMOUNT" ] || [ -z "$CURRENT_CURRENCY" ]; then
echo "No cheapest live price found; skipping alert."
exit 0
fi

if [ "$CURRENT_CURRENCY" != "$ALERT_CURRENCY" ]; then
echo "::warning::Price alert currency is $ALERT_CURRENCY but Google Flights returned $CURRENT_CURRENCY. Alert comparison skipped."
exit 0
fi

TITLE="✈️ Flight price alert · China 2026"
ISSUE=$(gh issue list --state open --json number,title --jq '.[] | select(.title == "✈️ Flight price alert · China 2026") | .number' | head -n 1)

if node -e "process.exit(Number(process.argv[1]) <= Number(process.argv[2]) ? 0 : 1)" "$CURRENT_AMOUNT" "$ALERT_AMOUNT"; then
BODY=$(cat <<EOF
## Price target reached 🎯

Latest Google Flights snapshot via SerpApi is at or below the configured target.

| | |
|---|---|
| **Current total** | ${CURRENT_AMOUNT} ${CURRENT_CURRENCY} |
| **Target** | ${ALERT_AMOUNT} ${ALERT_CURRENCY} |
| **Airline** | ${CURRENT_AIRLINE} |
| **Scenario** | ${CURRENT_SCENARIO} |
| **Checked** | ${CHECKED_AT} |
| **Travellers** | 6 adults + 1 infant under 2 on lap |

Open the live dashboard: https://trips.eplus.dev/flights.html

> Search prices can change. Verify the itinerary and final booking price before paying.
EOF
)

if [ -n "$ISSUE" ]; then
gh issue edit "$ISSUE" --body "$BODY"
echo "Updated existing price alert issue #$ISSUE."
else
gh issue create --title "$TITLE" --body "$BODY"
echo "Created a new price alert issue."
fi
else
echo "Current price ${CURRENT_AMOUNT} ${CURRENT_CURRENCY} is above target ${ALERT_AMOUNT} ${ALERT_CURRENCY}."
if [ -n "$ISSUE" ]; then
gh issue close "$ISSUE" --comment "Latest price moved back above the target: ${CURRENT_AMOUNT} ${CURRENT_CURRENCY} (target ${ALERT_AMOUNT} ${ALERT_CURRENCY}). The next target hit can create a fresh alert."
fi
fi

- name: Commit refreshed price snapshot
if: steps.config.outputs.enabled == 'true'
id: commit
run: |
if git diff --quiet -- data/flights.json data/flight-history.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No flight price changes to commit."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add data/flights.json data/flight-history.json
git commit -m "chore: refresh Google Flights prices [skip ci]"
git push origin "HEAD:${GITHUB_REF_NAME}"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Request GitHub Pages rebuild
if: steps.config.outputs.enabled == 'true' && steps.commit.outputs.changed == 'true' && github.ref_name == 'main'
env:
GH_TOKEN: ${{ github.token }}
run: |
curl --fail-with-body -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/pages/builds"
166 changes: 125 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,145 @@
# Personal Travel Log

A lightweight personal travel dashboard built for GitHub Pages. It works as a static site, stores personal planning data locally in the browser, and can also behave like a small installable web app.
A lightweight personal travel dashboard for GitHub Pages with itinerary, budget, notes, PWA support and automated Google Flights price tracking.

## Features

- Responsive desktop and mobile layout
- Live countdown to the next departure
- Visual trip route and overview statistics
- Trip essentials: flights, hotels, internet and map shortcuts
- Expandable day-by-day itinerary
- Upcoming destination cards
- Pre-trip checklist with completion percentage
- Budget tracker with planned / actual / remaining totals
- Personal trip notes saved automatically in `localStorage`
- Export / import local travel data as JSON
- Native share button when supported
- Light / dark mode
- Mobile bottom navigation
- PWA manifest + service worker for install/offline use
- No framework, database or build process required
- Responsive desktop/mobile travel dashboard
- Countdown, route, itinerary and destination cards
- Checklist, budget and notes saved in `localStorage`
- Export/import local travel data
- Light/dark mode and PWA/offline support
- **Google Flights price snapshots through SerpApi + GitHub Actions**
- Return-date comparison, airline/stops filters, price history and price alerts
- No separate backend server

## Files
## Current China trip

- Ho Chi Minh City → Shanghai → Beijing
- Outbound: **20 October 2026**
- Return options: **25 October evening** or **26 October morning**
- **6 adults + 1 infant under 2 on lap**
- Economy
- Direct or maximum 1 stop per leg

## Live flight prices

The workflow `.github/workflows/update-flight-prices.yml` calls SerpApi's Google Flights engine. The API key remains in GitHub Actions Secrets and is never exposed in the browser.

Google Flights multi-city selection is sequential. For each return-date scenario the fetcher performs:

1. initial multi-city search to get the first-leg options and a `departure_token`,
2. a second search with that token to get the next leg and complete itinerary prices.

There are two scenarios, so one refresh uses **4 SerpApi searches**.

### 1. Create a SerpApi key

Create a SerpApi account and copy your private API key.

### 2. Add the GitHub Actions secret

Repository → **Settings → Secrets and variables → Actions → New repository secret**

```text
trip/
├── index.html
├── manifest.webmanifest
├── sw.js
├── icon.svg
├── .nojekyll
└── README.md
Name: SERPAPI_API_KEY
Value: <your SerpApi API key>
```

## Current trip
Do not add the key to source code, repository variables, `flights.json`, issues or PR comments.

The starter data is configured for the China trip in October 2026:
### 3. Merge the PR and run the first check

- Ho Chi Minh City → Shanghai → Beijing
- 20–26 October 2026
- Day-by-day itinerary included in `index.html`
After the workflow exists on `main`:

```text
Actions
→ Update live flight prices
→ Run workflow
```

The workflow writes:

```text
data/flights.json
data/flight-history.json
```

and commits refreshed snapshots back to `main`.

The site intentionally keeps travel data in plain HTML/JavaScript so it is easy to edit directly from GitHub without a build pipeline.
### Automatic refresh

## Personal data
The default schedule is:

Checklist, budget and notes are stored in the browser using `localStorage` under:
```text
07:17 Asia/Ho_Chi_Minh
```

One refresh uses 4 API searches, so a 30-day month is roughly **120 searches**, leaving room for manual checks within SerpApi's free quota.

### Price dashboard

`flights.html` provides:

- comparison of returning **25 vs 26 October**
- Cheapest / Fastest sorting
- Direct only / 1 stop filters
- airline filter
- total search price for the selected 7 travellers
- rough total ÷ 7 reference value
- price change from the previous check
- lowest/highest saved prices
- saved trend chart
- Fresh / Aging / Stale indicator
- browser-local target price
- shortcut to run GitHub Actions manually

## Optional GitHub Issue price alert

Create repository Actions variables:

```text
travel-log-v2
FLIGHT_ALERT_AMOUNT=30000000
FLIGHT_ALERT_CURRENCY=VND
```

Use **Export data** before changing browsers/devices. The exported JSON file can later be restored with **Import data**.
When the current cheapest total is at or below the threshold, the workflow opens or updates:

```text
✈️ Flight price alert · China 2026
```

## Run locally
When the price moves above the target again, the issue is closed.

## Price notes

The results are Google Flights search snapshots, not locked fares. Google Flights may omit some carriers/options and final seller prices can change. Baggage, card and other optional fees may be additional. Always verify the itinerary and final amount on Google Flights or the airline/agency before paying.

## Files

```text
trips/
├── .github/workflows/
│ ├── pr-preview.yml
│ └── update-flight-prices.yml
├── data/
│ ├── flights.json
│ └── flight-history.json
├── scripts/
│ └── fetch-flights.mjs
├── flights/
│ └── index.html
├── flights.html
├── index.html
├── manifest.webmanifest
├── sw.js
├── icon.svg
├── CNAME
├── .nojekyll
└── README.md
```

## Local development

```bash
python3 -m http.server 8080
Expand All @@ -61,22 +148,19 @@ python3 -m http.server 8080
Open:

```text
http://localhost:8080
http://localhost:8080/
http://localhost:8080/flights.html
```

Using a local server is recommended when testing the service worker and PWA behavior.

## GitHub Pages

Repository Settings → Pages:

```text
Source: Deploy from a branch
Branch: main
Folder: / (root)
```

The `.nojekyll` file keeps GitHub Pages in simple static-site mode.
The refresh workflow explicitly requests a Pages rebuild after committing price data because a commit pushed by a workflow `GITHUB_TOKEN` does not itself trigger another Pages build.

---

Expand Down
1 change: 1 addition & 0 deletions data/flight-history.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Loading
Loading