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
2 changes: 1 addition & 1 deletion .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
const pr = context.payload.pull_request;
const sha = pr.head.sha;
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- [Open live hotel prices](${root}/hotels.html)\n\nPreview is pinned to commit \`${sha.slice(0, 7)}\` and updates automatically when the PR changes.`;
const body = `${marker}\n## 🌍 Web preview\n\n- [Open travel dashboard](${root}/index.html)\n- [Open live flight prices](${root}/flights.html)\n- [Open flight status tracker](${root}/flight-status.html)\n- [Open live hotel prices](${root}/hotels.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 Down
71 changes: 71 additions & 0 deletions .github/workflows/update-flight-status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Update flight status

on:
workflow_dispatch:
schedule:
# Daily baseline/live check at 07:37 Vietnam time.
- cron: '37 0 * * *'
# Hourly around the two travel dates. The script skips dates outside 2026 targets.
- cron: '37 * 18-20,25-27 10 *'

permissions:
contents: write
pages: write

concurrency:
group: flight-status-${{ 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 tracked flight status
env:
SERPAPI_API_KEY: ${{ secrets.SERPAPI_API_KEY }}
SERPAPI_API_KEY_2: ${{ secrets.SERPAPI_API_KEY_2 }}
SERPAPI_API_KEY_3: ${{ secrets.SERPAPI_API_KEY_3 }}
SERPAPI_BOOKING_API_KEY: ${{ secrets.SERPAPI_BOOKING_API_KEY }}
FLIGHT_STATUS_LIVE_WINDOW_DAYS: '7'
FORCE_FLIGHT_STATUS: ${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }}
run: node scripts/fetch-flight-status.mjs

- name: Commit status snapshot
id: commit
shell: bash
run: |
if git diff --quiet -- data/flight-status.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No flight-status 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/flight-status.json
git commit -m "chore: refresh tracked flight status [skip ci]"
git push origin "HEAD:${GITHUB_REF_NAME}"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Request GitHub Pages rebuild
if: 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"
106 changes: 106 additions & 0 deletions data/flight-status.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"generated_at": "2026-08-29T08:42:00.000Z",
"provider": "TravelLog schedule baseline",
"live_mode": false,
"message": "Live operational status is usually published closer to departure. The tracker will start automatic live checks 7 days before each flight and can also be run manually.",
"flights": [
{
"id": "VJ3900-2026-10-19",
"flight_number": "VJ3900",
"display_flight_number": "VJ 3900",
"date": "2026-10-19",
"airline": "Vietjet",
"airline_iata_code": "VJ",
"airline_logo": "https://www.gstatic.com/flights/airline_logos/70px/VJ.png",
"aircraft": "Airbus A321",
"route": {
"origin": "SGN",
"origin_name": "TP. Hồ Chí Minh",
"destination": "PVG",
"destination_name": "Thượng Hải Pudong"
},
"status": {
"code": "scheduled",
"label": "Theo lịch",
"raw": "Scheduled",
"live": false
},
"departure": {
"scheduled": "2026-10-19T20:00:00+07:00",
"estimated": null,
"actual": null,
"time_zone": "Asia/Ho_Chi_Minh",
"terminal": null,
"gate": null,
"delay_minutes": 0
},
"arrival": {
"scheduled": "2026-10-20T01:00:00+08:00",
"estimated": null,
"actual": null,
"time_zone": "Asia/Shanghai",
"terminal": null,
"gate": null,
"delay_minutes": 0
},
"schedule_source": "Google Flights snapshot in TravelLog",
"checked_at": null,
"latest_update": null,
"next_live_check_at": "2026-10-12T00:00:00.000Z",
"provider_sources": [],
"external_links": {
"google": "https://www.google.com/search?q=VJ3900+flight+status",
"airline": "https://www.vietjetair.com/"
}
},
{
"id": "CZ3106-2026-10-26",
"flight_number": "CZ3106",
"display_flight_number": "CZ 3106",
"date": "2026-10-26",
"airline": "China Southern Airlines",
"airline_iata_code": "CZ",
"airline_logo": "https://www.gstatic.com/flights/airline_logos/70px/CZ.png",
"aircraft": "Airbus A350",
"route": {
"origin": "PKX",
"origin_name": "Bắc Kinh Daxing",
"destination": "CAN",
"destination_name": "Quảng Châu Baiyun"
},
"status": {
"code": "scheduled",
"label": "Theo lịch",
"raw": "Scheduled",
"live": false
},
"departure": {
"scheduled": "2026-10-26T13:30:00+08:00",
"estimated": null,
"actual": null,
"time_zone": "Asia/Shanghai",
"terminal": null,
"gate": null,
"delay_minutes": 0
},
"arrival": {
"scheduled": "2026-10-26T16:45:00+08:00",
"estimated": null,
"actual": null,
"time_zone": "Asia/Shanghai",
"terminal": "2",
"gate": null,
"delay_minutes": 0
},
"schedule_source": "Booked itinerary",
"checked_at": null,
"latest_update": null,
"next_live_check_at": "2026-10-19T00:00:00.000Z",
"provider_sources": [],
"external_links": {
"google": "https://www.google.com/search?q=CZ3106+flight+status",
"airline": "https://www.csair.com/cn/status/"
}
}
]
}
59 changes: 59 additions & 0 deletions flight-status.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.status-content{width:min(1120px,calc(100% - 44px))}
.status-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:14px}
.status-heading h1{margin:4px 0 5px;font-size:27px;line-height:1.15;letter-spacing:-.035em}
.status-heading p{margin:0;color:var(--muted);font-size:12.5px;max-width:680px}
.status-kicker{font-size:10.5px;color:var(--primary);font-weight:800;text-transform:uppercase;letter-spacing:.07em}
.status-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.status-overview{display:grid;grid-template-columns:1.1fr .9fr .9fr;overflow:hidden;margin-bottom:12px}
.status-overview-item{padding:16px 18px;min-height:86px;border-right:1px solid var(--line)}
.status-overview-item:last-child{border-right:0}
.status-overview-item span{display:block;font-size:10.5px;color:var(--muted);font-weight:650}
.status-overview-item strong{display:block;font-size:16px;margin-top:5px;letter-spacing:-.015em}
.status-overview-item small{display:block;color:var(--muted);font-size:10.5px;margin-top:2px}
.status-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.status-card{overflow:hidden}
.status-card-head{padding:16px 17px 13px;display:flex;align-items:center;justify-content:space-between;gap:14px;border-bottom:1px solid var(--line)}
.status-airline{display:flex;align-items:center;gap:10px;min-width:0}
.status-airline-logo{width:36px;height:36px;border:1px solid var(--line);border-radius:10px;background:#fff;display:grid;place-items:center;overflow:hidden;flex:0 0 auto}
.status-airline-logo img{max-width:29px;max-height:29px}
.status-airline strong{display:block;font-size:14px}
.status-airline span{display:block;font-size:10.5px;color:var(--muted);margin-top:1px}
.status-badge{display:inline-flex;align-items:center;gap:6px;min-height:25px;padding:0 9px;border-radius:999px;font-size:10.5px;font-weight:800;white-space:nowrap;background:var(--primary-soft);color:var(--primary)}
.status-badge:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
.status-badge.on_time,.status-badge.landed{background:var(--success-soft);color:var(--success)}
.status-badge.delayed{background:var(--warning-soft);color:var(--warning)}
.status-badge.cancelled{background:color-mix(in srgb,var(--danger) 12%,var(--surface));color:var(--danger)}
.status-badge.in_air,.status-badge.boarding,.status-badge.departed{background:var(--primary-soft);color:var(--primary)}
.status-route{padding:18px 17px 13px;display:grid;grid-template-columns:1fr minmax(105px,.8fr) 1fr;align-items:center;gap:10px}
.status-airport strong{display:block;font-size:24px;letter-spacing:-.035em;line-height:1}
.status-airport span{display:block;color:var(--muted);font-size:10.5px;margin-top:5px}
.status-airport.right{text-align:right}
.status-path{text-align:center;color:var(--muted)}
.status-path-line{height:1px;background:var(--line-2);position:relative;margin:8px 5px}
.status-path-line:after{content:"✈";position:absolute;left:50%;top:50%;transform:translate(-50%,-53%);background:var(--surface);padding:0 6px;color:var(--primary);font-size:14px}
.status-path small{font-size:9.5px;color:var(--muted)}
.status-times{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:var(--surface-2)}
.status-time{padding:13px 17px}
.status-time+ .status-time{border-left:1px solid var(--line)}
.status-time span{display:block;color:var(--muted);font-size:10px;font-weight:650}
.status-time strong{display:block;font-size:17px;margin-top:3px}
.status-time small{display:block;color:var(--muted);font-size:10.5px;margin-top:2px}
.status-live-grid{display:grid;grid-template-columns:repeat(4,1fr);padding:12px 17px;gap:8px}
.status-detail{min-width:0}
.status-detail span{display:block;font-size:9.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-weight:700}
.status-detail strong{display:block;font-size:11.5px;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.status-note{margin:0 17px 13px;padding:10px 11px;border-radius:9px;background:var(--surface-2);border:1px solid var(--line);color:var(--muted);font-size:10.5px}
.status-note.live{background:var(--success-soft);border-color:transparent;color:var(--success)}
.status-card-foot{padding:0 17px 16px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.status-card-foot .status-source{margin-left:auto;color:var(--muted);font-size:9.5px;text-align:right}
.status-help{margin-top:12px;padding:16px 18px;display:grid;grid-template-columns:1.1fr .9fr;gap:22px}
.status-help h2{font-size:14px;margin:0 0 5px}
.status-help p{margin:0;color:var(--muted);font-size:11.5px}
.status-help-list{display:grid;gap:7px}
.status-help-item{display:flex;align-items:flex-start;gap:8px;font-size:10.5px;color:var(--muted)}
.status-help-item b{color:var(--text)}
.status-help-dot{width:7px;height:7px;border-radius:50%;background:var(--primary);margin-top:4px;flex:0 0 auto}
.status-loading{padding:34px;text-align:center;color:var(--muted)}
.status-error{padding:18px;color:var(--danger);text-align:center}
@media(max-width:850px){.status-grid{grid-template-columns:1fr}.status-help{grid-template-columns:1fr}.status-live-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:700px){.status-content{width:min(100% - 24px,1120px);padding-top:16px}.status-heading{display:block}.status-actions{margin-top:12px}.status-overview{grid-template-columns:1fr}.status-overview-item{border-right:0;border-bottom:1px solid var(--line);min-height:auto}.status-overview-item:last-child{border-bottom:0}.status-route{grid-template-columns:1fr 90px 1fr}.status-airport strong{font-size:21px}.status-card-foot .status-source{width:100%;margin-left:0;text-align:left}.status-heading h1{font-size:24px}}
76 changes: 76 additions & 0 deletions flight-status.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#f6f8fc">
<meta name="description" content="Theo dõi trạng thái VJ3900 ngày 19/10/2026 và CZ3106 ngày 26/10/2026.">
<link rel="icon" href="./icon.svg" type="image/svg+xml">
<link rel="stylesheet" href="./app.css">
<link rel="stylesheet" href="./flight-status.css">
<title>Trạng thái chuyến bay · TravelLog</title>
</head>
<body>
<div class="app">
<aside class="sidebar">
<a class="brand" href="./index.html"><span class="brand-mark"><svg><use href="#i-plane"/></svg></span>TravelLog</a>
<nav class="side-nav">
<a href="./index.html"><span class="nav-icon"><svg><use href="#i-home"/></svg></span>Tổng quan</a>
<a href="./index.html#itinerary"><span class="nav-icon"><svg><use href="#i-calendar"/></svg></span>Lịch trình</a>
<a href="./flights.html"><span class="nav-icon"><svg><use href="#i-plane"/></svg></span>Giá vé</a>
<a class="active" href="./flight-status.html"><span class="nav-icon"><svg><use href="#i-status"/></svg></span>Trạng thái</a>
<a href="./hotels.html"><span class="nav-icon"><svg><use href="#i-pin"/></svg></span>Giá phòng</a>
<a href="./index.html#budget"><span class="nav-icon"><svg><use href="#i-wallet"/></svg></span>Ngân sách</a>
<a href="./index.html#checklist"><span class="nav-icon"><svg><use href="#i-check"/></svg></span>Chuẩn bị</a>
<a href="./index.html#notes"><span class="nav-icon"><svg><use href="#i-note"/></svg></span>Ghi chú</a>
</nav>
<div class="sidebar-foot"><button class="sidebar-action" data-theme-toggle><span class="nav-icon"><svg><use href="#i-moon"/></svg></span>Đổi giao diện</button></div>
</aside>
<div class="main">
<header class="topbar">
<div class="crumb"><strong>Trạng thái chuyến bay</strong><span>Hành trình Trung Quốc 2026</span></div>
<div class="top-actions"><a class="btn sm" href="./flights.html">Giá vé</a><a class="btn sm" href="./index.html">Về tổng quan</a><button class="icon-btn" data-theme-toggle aria-label="Chuyển sang giao diện tối"><svg class="ui-icon"><use href="#i-moon"/></svg></button></div>
</header>
<header class="mobile-header"><a class="brand" href="./index.html"><span class="brand-mark"><svg><use href="#i-plane"/></svg></span>TravelLog</a><div class="right"><button class="icon-btn" data-theme-toggle aria-label="Chuyển sang giao diện tối"><svg class="ui-icon"><use href="#i-moon"/></svg></button></div></header>
<main class="content status-content">
<section class="status-heading">
<div><span class="status-kicker">Flight status</span><h1>Trạng thái chuyến bay đã đặt</h1><p id="statusMessage">Theo dõi lịch bay và trạng thái vận hành cho hai chuyến quan trọng trong hành trình.</p></div>
<div class="status-actions"><a class="btn" href="./flights.html">Xem giá vé</a><a class="btn primary" href="https://github.com/ePlus-DEV/trips/actions/workflows/update-flight-status.yml" target="_blank" rel="noopener"><svg class="ui-icon"><use href="#i-refresh"/></svg>Kiểm tra ngay</a></div>
</section>

<section class="card status-overview">
<div class="status-overview-item"><span>CHUYẾN ĐANG THEO DÕI</span><strong><span id="trackedCount">2</span> chuyến</strong><small>VJ 3900 · CZ 3106</small></div>
<div class="status-overview-item"><span>TRẠNG THÁI DỮ LIỆU</span><strong id="dataMode">Đang tải…</strong><small><span id="liveCount">0</span> chuyến có dữ liệu live</small></div>
<div class="status-overview-item"><span>CẬP NHẬT GẦN NHẤT</span><strong id="updatedAt">—</strong><small>Giờ Việt Nam (UTC+7)</small></div>
</section>

<section id="statusGrid" class="status-grid" aria-live="polite"><div class="card status-loading">Đang tải trạng thái chuyến bay…</div></section>

<section class="card status-help">
<div><h2>Khi nào trạng thái live xuất hiện?</h2><p>Trạng thái vận hành như đúng giờ, trễ, gate, boarding hoặc đã cất cánh thường chỉ đáng tin cậy khi gần ngày bay. TravelLog giữ nguyên lịch đã đặt làm mốc và không lấy dữ liệu của ngày khác để thay thế.</p></div>
<div class="status-help-list">
<div class="status-help-item"><span class="status-help-dot"></span><span><b>Trước 7 ngày:</b> bắt đầu kiểm tra tự động mỗi ngày.</span></div>
<div class="status-help-item"><span class="status-help-dot"></span><span><b>Quanh ngày bay:</b> kiểm tra tự động mỗi giờ.</span></div>
<div class="status-help-item"><span class="status-help-dot"></span><span><b>Kiểm tra ngay:</b> chạy thủ công GitHub Action bất cứ lúc nào.</span></div>
</div>
</section>
</main>
</div>
</div>
<nav class="mobile-bottom"><a href="./index.html"><svg><use href="#i-home"/></svg>Tổng quan</a><a href="./flights.html"><svg><use href="#i-plane"/></svg>Giá vé</a><a class="active" href="./flight-status.html"><svg><use href="#i-status"/></svg>Trạng thái</a><a href="./hotels.html"><svg><use href="#i-pin"/></svg>Giá phòng</a></nav>
<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true">
<symbol id="i-plane" viewBox="0 0 24 24"><path d="M10.5 4h3l2.2 6H20a2 2 0 1 1 0 4h-4.3l-2.2 6h-3l1.2-6H7l-2 2H3l1.2-4L3 8h2l2 2h4.7l-1.2-6Z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24"><path d="m3 11 9-8 9 8v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1Z"/></symbol>
<symbol id="i-calendar" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2.5"/><path d="M16 3v4M8 3v4M3 10h18"/></symbol>
<symbol id="i-wallet" viewBox="0 0 24 24"><path d="M4 6h14a2 2 0 0 1 2 2v11H4a2 2 0 0 1-2-2V6a3 3 0 0 1 3-3h12"/><path d="M16 11h5v5h-5a2.5 2.5 0 0 1 0-5Z"/></symbol>
<symbol id="i-note" viewBox="0 0 24 24"><rect x="4" y="3" width="16" height="18" rx="2.5"/><path d="M8 8h8M8 12h8M8 16h5"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="4"/><path d="m8 12 2.5 2.5L16.5 9"/></symbol>
<symbol id="i-pin" viewBox="0 0 24 24"><path d="M20 10c0 5-8 12-8 12S4 15 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></symbol>
<symbol id="i-status" viewBox="0 0 24 24"><path d="M4 17h4l2-10 4 13 2-8h4"/><path d="M3 4h18v16H3z"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.42-1.42M17.66 6.34l1.41-1.41"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0 1 4"/><path d="M20 4v7h-7"/></symbol>
</svg>
<script src="./flight-status.js"></script>
</body>
</html>
Loading
Loading