Project: RNR_20Aug2026 ZIP File: public_html(1).zip Extracted on: 2026-08-20 02:40:33 Total Files: 43 ================================================================================ FILE: availability.php TYPE: PHP SIZE: 6.6 KB ------------------------------------------------------------ false, 'message' => 'Invalid date.']); if ($train === '' || $from === '' || $to === '') avail_out(['status' => false, 'message' => 'Missing parameters.']); if (!in_array($cls, ['SL', '3A', '2A', '1A', 'CC', 'EC', '2S', '3E', 'FC'], true)) $cls = 'SL'; $pdo = db(); $pdo->exec("CREATE TABLE IF NOT EXISTS avail_cache (ckey CHAR(40) PRIMARY KEY, payload MEDIUMTEXT, created_at INT)"); $pdo->exec("CREATE TABLE IF NOT EXISTS rapidapi_usage (ym CHAR(6) PRIMARY KEY, calls INT NOT NULL DEFAULT 0)"); $pdo->exec("CREATE TABLE IF NOT EXISTS train_classes (train_no VARCHAR(10), class VARCHAR(4), valid TINYINT, updated_at INT, PRIMARY KEY (train_no, class))"); /* ---- analytics: count this availability check (cached/short-circuited ones count too) ---- */ $__an = __DIR__ . '/app/analytics.php'; if (is_file($__an)) { require_once $__an; rr_track('availability', ['train_no' => $train, 'from_code' => $from, 'to_code' => $to, 'jdate' => $date, 'note' => $cls . '/' . $quota]); } /* ---- short-circuit: we already learned this train has no such class ---- */ $kc = $pdo->prepare("SELECT valid FROM train_classes WHERE train_no = ? AND class = ?"); $kc->execute([$train, $cls]); $known = $kc->fetchColumn(); if ($known !== false && (int) $known === 0) { avail_out(['status' => false, 'noclass' => true, 'message' => "This train doesn't have $cls class."]); } /* ---- cache (success or cached rejection), per exact query ---- */ $ckey = sha1("$train|$from|$to|$cls|$quota|$date"); $cr = $pdo->prepare("SELECT payload, created_at FROM avail_cache WHERE ckey = ?"); $cr->execute([$ckey]); $c = $cr->fetch(PDO::FETCH_ASSOC); if ($c && (time() - (int) $c['created_at'] < AVAIL_CACHE_TTL)) { $p = json_decode($c['payload'], true); if (!empty($p['ok'])) avail_out(['status' => true, 'cached' => true, 'class' => $cls, 'data' => $p['data']]); avail_out(['status' => false, 'cached' => true, 'apierror' => true, 'message' => $p['err'] ?? 'This train/segment can\'t be checked here.']); } /* ---- monthly budget guard ---- */ $ym = date('Ym'); $uq = $pdo->prepare("SELECT calls FROM rapidapi_usage WHERE ym = ?"); $uq->execute([$ym]); $used = (int) ($uq->fetchColumn() ?: 0); if ($used >= AVAIL_MONTHLY_BUDGET) { avail_out(['status' => false, 'budget' => true, 'message' => 'Live availability is paused for this month. Please check on the RailOne app or IRCTC.']); } $apiKey = defined('RAPIDAPI_KEY') ? RAPIDAPI_KEY : (getenv('RAPIDAPI_KEY') ?: ''); if ($apiKey === '') avail_out(['status' => false, 'message' => 'Availability service not configured.']); $qs = http_build_query([ 'classType' => $cls, 'fromStationCode' => $from, 'quota' => $quota, 'toStationCode' => $to, 'trainNo' => $train, 'date' => $date, ]); $ch = curl_init(AVAIL_ENDPOINT . '?' . $qs); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => ['x-rapidapi-key: ' . $apiKey, 'x-rapidapi-host: ' . AVAIL_HOST], ]); $resp = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($resp === false) { avail_out(['status' => false, 'message' => 'Could not reach the availability service. Try again, or check on RailOne / IRCTC.']); } if ($code === 429) { avail_out(['status' => false, 'budget' => true, 'message' => 'Availability is busy right now. Please check on the RailOne app or IRCTC.']); } $json = json_decode($resp, true); if ($code >= 400 || !is_array($json)) { avail_out(['status' => false, 'http' => $code, 'message' => 'Could not fetch availability right now. Try again, or check on RailOne / IRCTC.']); } /* A JSON body came back (HTTP 200) — this spends one call. Count it. */ $pdo->prepare("INSERT INTO rapidapi_usage (ym, calls) VALUES (?, 1) ON DUPLICATE KEY UPDATE calls = calls + 1")->execute([$ym]); if (empty($json['status'])) { $raw = (string) ($json['message'] ?? ''); /* Learn "class does not exist" so we never spend another credit on it. */ if (stripos($raw, 'class') !== false && (stripos($raw, 'exist') !== false || stripos($raw, 'not available') !== false)) { $pdo->prepare("REPLACE INTO train_classes (train_no, class, valid, updated_at) VALUES (?, ?, 0, ?)") ->execute([$train, $cls, time()]); avail_out(['status' => false, 'noclass' => true, 'message' => "This train doesn't have $cls class."]); } $msg = (stripos($raw, 'valid train') !== false || $raw === '') ? 'Live availability isn\'t available for this train/segment here. You can check on IRCTC or the RailOne app.' : $raw; $pdo->prepare("REPLACE INTO avail_cache (ckey, payload, created_at) VALUES (?, ?, ?)") ->execute([$ckey, json_encode(['ok' => false, 'err' => $msg]), time()]); avail_out(['status' => false, 'apierror' => true, 'message' => $msg]); } /* Success — remember this class is valid for the train. */ $pdo->prepare("REPLACE INTO train_classes (train_no, class, valid, updated_at) VALUES (?, ?, 1, ?)") ->execute([$train, $cls, time()]); $data = $json['data'] ?? []; $pdo->prepare("REPLACE INTO avail_cache (ckey, payload, created_at) VALUES (?, ?, ?)") ->execute([$ckey, json_encode(['ok' => true, 'data' => $data]), time()]); avail_out(['status' => true, 'cached' => false, 'class' => $cls, 'data' => $data]); -------------------- END OF FILE -------------------- FILE: dashboard.php TYPE: PHP SIZE: 1.3 KB ------------------------------------------------------------

Welcome,

Find a train

Admin

You have administrator access.

Open admin panel
-------------------- END OF FILE -------------------- FILE: default.php TYPE: PHP SIZE: 15.99 KB ------------------------------------------------------------ Default page

You Are All Set to Go!

All you have to do now is upload your website files and start your journey. Check out how to do that below:

-------------------- END OF FILE -------------------- FILE: disclaimer.php TYPE: PHP SIZE: 2.58 KB ------------------------------------------------------------ ← Home

Disclaimer

Last updated:

is a private, independent service and is NOT affiliated with, endorsed by, or connected to IRCTC, the Indian Railways, the Ministry of Railways, or any government body.

Information is indicative — verify before you travel

The train numbers, timings, running days, routes, and connecting journeys shown on are compiled from a reference timetable and third-party data for general planning convenience. This information may be incomplete, outdated, or inaccurate, and the running days of some trains may not be verified.

Always confirm train details with official and reliable railway sources — such as IRCTC (irctc.co.in), the National Train Enquiry System (NTES, enquiry.indianrail.gov.in), or your railway station — before booking tickets or travelling. Do not rely solely on this Service for travel decisions.

No ticketing

We do not sell tickets, make reservations, or process any payments for rail travel. The Service is purely an informational route planner.

No liability

We accept no responsibility or liability for any loss, inconvenience, or damage — including missed trains or connections — arising from the use of, or reliance on, information provided by the Service. Use of the Service is entirely at your own risk.

Trademarks

All railway, train, and station names and trademarks are the property of their respective owners and are used here for identification and informational purposes only.

Advertising

This Service displays advertisements, including from third-party networks. Advertiser content and offers are the responsibility of the respective advertisers; we do not endorse them.

Contact

Questions: .

Privacy Policy · Terms & Conditions

-------------------- END OF FILE -------------------- FILE: index.php TYPE: PHP SIZE: 4.19 KB ------------------------------------------------------------

How it works

Enter two stations and a date. We show every train running that day — direct trains and connecting journeys with up to three changes, where a transfer can be at any station two trains share, not only the obvious junctions. Sort and filter the results by travel time, number of trains, departure or arrival, and reserved vs unreserved.

Popular routes

Erode → Chennai Coimbatore → Chennai Bengaluru → Chennai Madurai → Chennai Erode → Bengaluru Trichy → Chennai Salem → Chennai Coimbatore → Bengaluru

Major stations

Chennai Central · KSR Bengaluru · Coimbatore Jn · Erode Jn · Madurai Jn · Tiruchchirapalli · Salem Jn

-------------------- END OF FILE -------------------- FILE: login.php TYPE: PHP SIZE: 1.07 KB ------------------------------------------------------------

Sign in

Use your Google account to continue. We never see or store a password.

Continue with Google
-------------------- END OF FILE -------------------- FILE: logout.php TYPE: PHP SIZE: 194 B ------------------------------------------------------------ exec("CREATE TABLE IF NOT EXISTS avail_cache (ckey CHAR(40) PRIMARY KEY, payload MEDIUMTEXT, created_at INT)"); $pdo->exec("CREATE TABLE IF NOT EXISTS rapidapi_usage (ym CHAR(6) PRIMARY KEY, calls INT NOT NULL DEFAULT 0)"); // analytics: count this PNR check (cached ones too) $__an = __DIR__ . '/app/analytics.php'; if (is_file($__an)) { require_once $__an; rr_track('pnr', ['note' => 'check']); } $ckey = sha1('pnr|' . $pnr); $cr = $pdo->prepare("SELECT payload, created_at FROM avail_cache WHERE ckey = ?"); $cr->execute([$ckey]); $c = $cr->fetch(PDO::FETCH_ASSOC); if ($c && (time() - (int) $c['created_at'] < PNR_CACHE_TTL)) { $p = json_decode($c['payload'], true); if (!empty($p['ok'])) { $data = $p['data']; $fromCache = true; } else { $error = $p['err'] ?? 'Could not fetch this PNR right now.'; } } else { // monthly budget guard $uq = $pdo->prepare("SELECT calls FROM rapidapi_usage WHERE ym = ?"); $uq->execute([date('Ym')]); $used = (int) ($uq->fetchColumn() ?: 0); if ($used >= PNR_BUDGET) { $error = 'Live PNR lookup is temporarily unavailable. Please check on the IRCTC / RailOne app.'; } else { $apiKey = defined('RAPIDAPI_KEY') ? RAPIDAPI_KEY : (getenv('RAPIDAPI_KEY') ?: ''); $ch = curl_init(PNR_ENDPOINT . '?pnrNumber=' . urlencode($pnr)); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_HTTPHEADER => ['x-rapidapi-key: ' . $apiKey, 'x-rapidapi-host: ' . PNR_HOST], ]); $resp = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($resp === false || $code === 0) { $error = 'Network issue reaching the railway server. Please try again.'; } elseif ($code === 429) { $error = 'Live PNR lookup is busy right now. Please try again shortly or check on the IRCTC / RailOne app.'; } else { // billed call $pdo->prepare("INSERT INTO rapidapi_usage (ym, calls) VALUES (?,1) ON DUPLICATE KEY UPDATE calls = calls + 1") ->execute([date('Ym')]); $j = json_decode($resp, true); if (is_array($j) && !empty($j['status']) && !empty($j['data']) && is_array($j['data'])) { $data = $j['data']; $pdo->prepare("REPLACE INTO avail_cache (ckey, payload, created_at) VALUES (?,?,?)") ->execute([$ckey, json_encode(['ok' => 1, 'data' => $data]), time()]); } else { $msg = is_array($j) ? ($j['message'] ?? '') : ''; $error = $msg !== '' ? $msg : 'PNR not found, or the details are not available yet. Newly booked or flushed PNRs may not show.'; $pdo->prepare("REPLACE INTO avail_cache (ckey, payload, created_at) VALUES (?,?,?)") ->execute([$ckey, json_encode(['ok' => 0, 'err' => $error]), time()]); } } } } } } $page_title = 'PNR status'; $meta_description = 'Check Indian Railways PNR status — confirmation chance, coach and berth, and chart status.'; require APP_DIR . '/header.php'; ?>

PNR status

Status updates as the chart is prepared. We show the latest from Indian Railways — booking is done on the IRCTC / RailOne app.

Boarding
Up to
Class
Quota
$px): if (!is_array($px)) continue; $bk = pv($px, ['bookingStatusDetails', 'bookingStatus', 'BookingStatus']); if ($bk === '') $bk = trim(pv($px, ['bookingCoachId', 'bookingBerthCode']) . ' ' . pv($px, ['bookingBerthNo'])); $cu = pv($px, ['currentStatusDetails', 'currentStatus', 'CurrentStatus']); if ($cu === '') $cu = trim(pv($px, ['currentCoachId', 'currentBerthCode']) . ' ' . pv($px, ['currentBerthNo'])); $sno = pv($px, ['passengerSerialNumber', 'Number'], (string) ($i + 1)); ?>
#Booking statusCurrent status
🎫 To book or manage this ticket, use Indian Railways' official IRCTC / RailOne app. Rails and Routes shows status only.
-------------------- END OF FILE -------------------- FILE: privacy.php TYPE: PHP SIZE: 6.25 KB ------------------------------------------------------------ ← Home

Privacy Policy

Last updated:

This Privacy Policy explains how ("we", "us", "our"), operator of (the "Service"), collects, uses, and protects information when you use the Service. By using the Service you agree to this Policy.

is a private, independent service. It is not affiliated with, endorsed by, or connected to IRCTC, the Indian Railways, or any government body.

1. Information we collect

We do not sell train tickets and do not collect payment-card details for ticketing.

2. How we use information

3. Legal basis

We process personal data on the basis of your consent (where required), the performance of our service to you, our legitimate interests in operating and improving the Service, and compliance with applicable law, including India's Digital Personal Data Protection Act, 2023.

4. Cookies

We use cookies and similar technologies to operate the Service, remember your session and preferences, gather analytics, and deliver advertising. You can control or disable cookies in your browser settings; however, some features may not function correctly without them.

5. Advertising

The Service displays advertisements, which may be provided by third-party advertising networks (for example, Google AdSense). These third parties may use cookies, device identifiers, and similar technologies to show ads based on your visits to this and other websites, and to measure ad performance. We do not control the data practices of these advertisers and networks.

You can manage personalised advertising through:

Please also review the privacy policies of any advertising partners we use.

6. How we share information

We do not sell your personal information. We may share information with: service providers who host and operate the Service on our behalf; advertising and analytics partners (as described above); and authorities or others where required by law or to protect rights, safety, and the integrity of the Service.

7. Data retention

We retain personal data only as long as necessary for the purposes described in this Policy, to comply with legal obligations, resolve disputes, and enforce our agreements. Account data is retained while your account is active and for a reasonable period afterward.

8. Data security

We apply reasonable technical and organisational measures to protect information, including encrypted connections (HTTPS) for data in transit, hashing of account passwords, restricted administrative access, and routine maintenance of our systems. No method of transmission or storage is completely secure, and we cannot guarantee absolute security. You are responsible for keeping your account credentials confidential.

9. Your rights

Subject to applicable law, you may have the right to access, correct, update, or delete your personal data, to withdraw consent, and to raise a grievance. To exercise these rights, contact us at . We will respond within the timeframes required by law.

10. Children

The Service is not directed to children, and we do not knowingly collect personal data from children. If you believe a child has provided us personal data, please contact us so we can remove it.

11. Third-party links

The Service may link to third-party sites and resources (including official railway sources). We are not responsible for the content or privacy practices of those sites.

12. Changes to this Policy

We may update this Policy from time to time. Material changes will be posted on this page with a revised "Last updated" date. Continued use of the Service after changes take effect constitutes acceptance.

13. Contact

For questions or grievances regarding this Policy or your data, contact us at . Operator location: .

Terms & Conditions · Disclaimer

-------------------- END OF FILE -------------------- FILE: robots.txt TYPE: TXT SIZE: 225 B ------------------------------------------------------------ User-agent: * Allow: / Disallow: /admin/ Disallow: /app/ Disallow: /login.php Disallow: /logout.php Disallow: /dashboard.php Disallow: /oauth-callback.php Disallow: /search.php Sitemap: https://railsandroutes.com/sitemap.php -------------------- END OF FILE -------------------- FILE: route.php TYPE: PHP SIZE: 4.98 KB ------------------------------------------------------------ prepare("SELECT name FROM stations WHERE code = ?"); $fn->execute([$from]); $fromName = $fn->fetchColumn() ?: $from; $tn = $pdo->prepare("SELECT name FROM stations WHERE code = ?"); $tn->execute([$to]); $toName = $tn->fetchColumn() ?: $to; $q = $pdo->prepare( "SELECT t.train_no, t.train_name, t.train_type, t.run_days, a.departure AS dep, b.arrival AS arr, (b.day_offset - a.day_offset) AS doff FROM train_stops a JOIN train_stops b ON b.train_no = a.train_no AND b.seq > a.seq JOIN trains t ON t.train_no = a.train_no WHERE a.station_code = ? AND b.station_code = ? AND t.run_days IS NOT NULL ORDER BY a.departure" ); $q->execute([$from, $to]); $rows = $q->fetchAll(PDO::FETCH_ASSOC); $cnt = count($rows); $page_title = 'Trains from ' . $fromName . ' to ' . $toName . ' — Time Table'; $meta_description = ($cnt ? $cnt . ' direct trains' : 'Direct & connecting trains') . ' from ' . $fromName . ' (' . $from . ') to ' . $toName . ' (' . $to . ') ' . 'with departure and arrival times, plus multi-train connecting journeys. Indicative times — verify before travel.'; $canonical = rr_abs(rr_route_url($from, $to)); $crumb = ['@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => [ ['@type' => 'ListItem', 'position' => 1, 'name' => 'Home', 'item' => rr_abs('/')], ['@type' => 'ListItem', 'position' => 2, 'name' => $fromName . ' to ' . $toName, 'item' => $canonical], ]]; $head_extra = ''; require APP_DIR . '/header.php'; ?>

Trains from to

· direct trains

Direct trains

No direct trains found. Use the planner above — connecting journeys (with a change of train) may still get you there.

Train Name Dep Arr Runs
0) ? ' +' . (int) $r['doff'] . 'd' : '' ?>

Our planner also finds connecting journeys — transfers at any shared station, not just major junctions. Indicative times; verify on official sources before travel.

-------------------- END OF FILE -------------------- FILE: search.php TYPE: PHP SIZE: 30.33 KB ------------------------------------------------------------ every train that runs that day $base = rt_plan($tt, $from, $to, 0, ['date' => $date, 'horizon' => 1440]); foreach ($base['direct'] as $l) $journeys[] = [$l]; // connecting options sampled across the day, de-duplicated $seen = []; foreach ([0, 360, 720, 1080] as $qd) { $r = ($qd === 0) ? $base : rt_plan($tt, $from, $to, $qd, ['date' => $date, 'horizon' => 1440]); foreach ($r['hops'] as $journey) { $sig = ''; foreach ($journey as $l) $sig .= $l['train'] . ':' . $l['board'] . '>'; if (isset($seen[$sig])) continue; $seen[$sig] = 1; $journeys[] = $journey; } } } catch (Throwable $ex) { error_log('router: ' . $ex->getMessage()); $err = 'The journey planner is warming up. Please try again in a moment.'; } } } // ---- Hide trains that have already left the origin (only for today's date, IST) ---- $departedAll = false; if ($journeys) { $nowIst = new DateTime('now', new DateTimeZone('Asia/Kolkata')); if ($date === $nowIst->format('Y-m-d')) { $nowMin = ((int) $nowIst->format('G')) * 60 + (int) $nowIst->format('i'); $before = count($journeys); $journeys = array_values(array_filter($journeys, function ($j) use ($nowMin) { return isset($j[0]['board']) && (int) $j[0]['board'] >= $nowMin; })); $departedAll = ($before > 0 && count($journeys) === 0); } } // ---- analytics: log the search (only when an actual A->B search ran) ---- if ($from !== '' && $to !== '') { $__an = APP_DIR . '/analytics.php'; if (is_file($__an)) { require_once $__an; rr_track('search', ['from_code' => $from, 'to_code' => $to, 'jdate' => $date, 'n' => count($journeys), 'note' => $departedAll ? 'departed' : '']); } } // ---- Names + types ---- $trainNos = []; $codes = [$from, $to]; foreach ($journeys as $j) foreach ($j as $l) { $trainNos[$l['train']] = 1; $codes[] = $l['from']; $codes[] = $l['to']; } $trainNames = []; $trainTypes = []; $stationNames = []; $trainClasses = []; if ($trainNos) { $in = implode(',', array_fill(0, count($trainNos), '?')); $hasClassesCol = true; try { $st = db()->prepare("SELECT train_no, train_name, train_type, classes FROM trains WHERE train_no IN ($in)"); $st->execute(array_keys($trainNos)); } catch (Throwable $e) { $hasClassesCol = false; $st = db()->prepare("SELECT train_no, train_name, train_type FROM trains WHERE train_no IN ($in)"); $st->execute(array_keys($trainNos)); } foreach ($st as $r) { $trainNames[$r['train_no']] = $r['train_name']; $trainTypes[$r['train_no']] = $r['train_type']; if ($hasClassesCol && isset($r['classes']) && $r['classes'] !== '' && $r['classes'] !== '-') { $vals = array_values(array_filter(array_map('trim', explode(',', (string) $r['classes'])))); if ($vals) $trainClasses[$r['train_no']] = ['valid' => $vals, 'invalid' => []]; } } try { $tcq = db()->prepare("SELECT train_no, class, valid FROM train_classes WHERE train_no IN ($in)"); $tcq->execute(array_keys($trainNos)); foreach ($tcq as $r) { $tn = $r['train_no']; if (!isset($trainClasses[$tn])) $trainClasses[$tn] = ['valid' => [], 'invalid' => []]; if ((int) $r['valid'] === 1) $trainClasses[$tn]['valid'][] = $r['class']; else $trainClasses[$tn]['invalid'][] = $r['class']; } } catch (Throwable $e) { /* learning table not created yet */ } } $codes = array_values(array_unique(array_filter($codes))); if ($codes) { $in = implode(',', array_fill(0, count($codes), '?')); $st = db()->prepare("SELECT code, name FROM stations WHERE code IN ($in)"); $st->execute($codes); foreach ($st as $r) $stationNames[$r['code']] = $r['name']; } // (class lists now come from trains.classes, populated by tools/backfill_classes.php — no per-search API call) function sname(string $c): string { global $stationNames; return $stationNames[$c] ?? $c; } function tname(string $t): string { global $trainNames; return $trainNames[$t] ?? ''; } function ttype(string $t): ?string { global $trainTypes; return $trainTypes[$t] ?? null; } function train_class(string $no, ?string $t): array { $u = strtoupper(trim((string) $t)); $isMEMU = strpos($u, 'ELECTRICAL MULTIPLE') !== false || strpos($u, 'MEMU') !== false; $isDEMU = (strpos($u, 'DIESEL') !== false && strpos($u, 'MULTIPLE') !== false) || strpos($u, 'DEMU') !== false; $tokenUnres = $isMEMU || $isDEMU || strpos($u, 'SUBURBAN') !== false || strpos($u, 'PASSENGER') !== false || strpos($u, 'EMU') !== false || in_array($u, ['SUB','PASS','MEX'], true); $reservedType = false; if ($u !== '') { foreach (['RAJDHANI','SHATABDI','DURONTO','SUPERFAST','SUF','GARIB RATH','HUMSAFAR','HMSR', 'VANDE BHARAT','VBEX','TEJAS','DOUBLE DECKER','MAIL EXPRESS','INTERCITY','SAMPARK KRANTI', 'SUVIDHA','RAJ','DRNT'] as $rk) { if (strpos($u, $rk) !== false) { $reservedType = true; break; } } } $d = ($no !== '') ? $no[0] : ''; $noUnres = in_array($d, ['3','4','5','6','7','9'], true); if ($reservedType) $unreserved = false; elseif ($tokenUnres) $unreserved = true; else $unreserved = $noUnres; $map = [ 'MAIL EXPRESS'=>'Mail/Express','SUPERFAST'=>'Superfast','SUF'=>'Superfast', 'INTERCITY'=>'Intercity','INT'=>'Intercity','GARIB RATH'=>'Garib Rath', 'SAMPARK KRANTI'=>'Sampark Kranti','SHATABDI'=>'Shatabdi','JAN SHATABDI'=>'Jan Shatabdi', 'RAJDHANI'=>'Rajdhani','RAJ'=>'Rajdhani','DURONTO'=>'Duronto','DRNT'=>'Duronto', 'HUMSAFAR'=>'Humsafar','HMSR'=>'Humsafar','VANDE BHARAT EXPRESS'=>'Vande Bharat','VBEX'=>'Vande Bharat', 'TEJAS EXPRESS'=>'Tejas','DOUBLE DECKER'=>'Double Decker','DD'=>'Double Decker', 'SUBURBAN'=>'Suburban','SUB'=>'Suburban','PASSENGER'=>'Passenger','PASS'=>'Passenger','MEX'=>'MEMU', ]; if ($isMEMU) $label = 'MEMU'; elseif ($isDEMU) $label = 'DEMU'; elseif (isset($map[$u])) $label = $map[$u]; elseif ($u !== '') $label = ucwords(strtolower($u)); else $label = ['3'=>'Suburban','4'=>'Suburban','5'=>'Passenger','6'=>'MEMU','7'=>'DEMU','9'=>'Suburban'][$d] ?? ''; return [$label, $unreserved]; } function rd_tag(array $l): string { if (!empty($l['harvested'])) { $days = $l['days'] ?? '—'; if (!empty($l['stale'])) return ' · Runs: ' . e($days) . ' · re-checking'; return ' · Runs: ' . e($days) . ''; } return ' · days not verified — assumed daily'; } $travelTs = strtotime($date . ' 00:00'); function whenstr(int $absMin): string { global $travelTs; $ts = $travelTs + $absMin * 60; return date('H:i', $ts) . ' · ' . date('D j M', $ts); } function station_node(string $code, string $kind, ?int $layover): void { ?>
🚉
wait
total tap for details ▾close ▴
🚆
dep arr
0): $w = $leg['board'] - $legs[$i - 1]['arr']; ?>
Change at () — wait
 Unreserved — general / unreserved ticket; book online on the RailOne app

dep

arr

Journeys

New search
Times are indicative (based on a reference timetable). All trains running on the selected date are shown. Always confirm on the official source before booking.
Reserved train (bookable online) Unreserved train (book on the RailOne app)
Travelling unreserved (general class)? Book your ticket online on Indian Railways’ official RailOne app. Google Play App Store

Enter an origin and destination to see journeys.

All trains from for today have already departed. Try tomorrow's date.No trains found for on . Try a nearby date or a different pair of stations.

-------------------- END OF FILE -------------------- FILE: sitemap.php TYPE: PHP SIZE: 1.46 KB ------------------------------------------------------------ ' . "\n"; $out .= '' . "\n"; $add = function (string $loc) use (&$out) { $out .= '' . htmlspecialchars($loc, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '' . "\n"; }; $add($base . '/'); $tr = $pdo->query("SELECT train_no FROM trains WHERE run_days IS NOT NULL ORDER BY train_no"); foreach ($tr as $row) { $add($base . rr_train_url($row['train_no'])); } $stq = $pdo->query( "SELECT DISTINCT s.code FROM stations s JOIN train_stops ts ON ts.station_code = s.code JOIN trains t ON t.train_no = ts.train_no AND t.run_days IS NOT NULL ORDER BY s.code" ); foreach ($stq as $row) { $add($base . rr_station_url($row['code'])); } $out .= '' . "\n"; @file_put_contents($cacheFile, $out); echo $out; -------------------- END OF FILE -------------------- FILE: station.php TYPE: PHP SIZE: 5.13 KB ------------------------------------------------------------ prepare("SELECT * FROM stations WHERE code = ?"); $sq->execute([$code]); $st = $sq->fetch(PDO::FETCH_ASSOC); if (!$st) { http_response_code(404); $page_title = 'Station ' . $code . ' not found'; $meta_robots = 'noindex'; require APP_DIR . '/header.php'; echo '

Station ' . e($code) . ' not found

Search journeys.

'; require APP_DIR . '/footer.php'; exit; } $tq = $pdo->prepare( "SELECT t.train_no, t.train_name, t.train_type, t.source_code, t.dest_code, t.run_days, ts.arrival, ts.departure FROM train_stops ts JOIN trains t ON t.train_no = ts.train_no WHERE ts.station_code = ? AND t.run_days IS NOT NULL ORDER BY COALESCE(ts.departure, ts.arrival)" ); $tq->execute([$code]); $rows = $tq->fetchAll(PDO::FETCH_ASSOC); $name = $st['name'] ?: $code; $cnt = count($rows); $page_title = 'Trains at ' . $name . ' (' . $code . ') — Time Table'; $meta_description = $cnt . ' trains stop at ' . $name . ' (' . $code . '). See arrival and departure times and plan direct or connecting journeys. Indicative times — verify before travel.'; $canonical = rr_abs(rr_station_url($code)); if ($cnt === 0) $meta_robots = 'noindex'; $crumb = ['@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => [ ['@type' => 'ListItem', 'position' => 1, 'name' => 'Home', 'item' => rr_abs('/')], ['@type' => 'ListItem', 'position' => 2, 'name' => $name . ' (' . $code . ')', 'item' => $canonical], ]]; $ld = ['@context' => 'https://schema.org', '@type' => 'TrainStation', 'name' => $name, 'identifier' => $code]; if (isset($st['lat'], $st['lng']) && $st['lat'] !== null && $st['lat'] !== '' && $st['lng'] !== null && $st['lng'] !== '') { $ld['geo'] = ['@type' => 'GeoCoordinates', 'latitude' => (float) $st['lat'], 'longitude' => (float) $st['lng']]; } $head_extra = '' . ''; require APP_DIR . '/header.php'; ?>

()

trains with a confirmed timetable stop here.

Trains calling at

No confirmed trains yet for this station.

Train Name Arr Dep Runs

Indicative times based on a reference timetable. Confirm on official sources before travel.

-------------------- END OF FILE -------------------- FILE: stations.php TYPE: PHP SIZE: 532 B ------------------------------------------------------------ prepare( 'SELECT code, name FROM stations WHERE name LIKE ? OR code LIKE ? ORDER BY (code = ?) DESC, CHAR_LENGTH(name) ASC LIMIT 10'); $st->execute([$like, $like, strtoupper($q)]); echo json_encode($st->fetchAll(), JSON_UNESCAPED_UNICODE); -------------------- END OF FILE -------------------- FILE: terms.php TYPE: PHP SIZE: 5.11 KB ------------------------------------------------------------ ← Home

Terms & Conditions

Last updated:

These Terms govern your use of (the "Service"), operated by ("we", "us", "our"). By accessing or using the Service, you agree to these Terms. If you do not agree, do not use the Service.

1. About the Service

The Service is an independent train journey-planning tool that suggests direct and connecting (multi-hop) routes between stations using a reference timetable and third-party data. It is provided for general information and planning convenience only.

2. No affiliation with IRCTC or Indian Railways

is a private, independent service. We are not affiliated with, authorised by, endorsed by, or connected to IRCTC, the Indian Railways, the Ministry of Railways, or any government authority. All railway names, train names, station names, and trademarks are the property of their respective owners and are used for identification and informational purposes only.

3. Accuracy and verification

Train numbers, timings, running days, routes, and connections shown on the Service are indicative only. They are derived from reference data that may be incomplete, outdated, or inaccurate, and running days for some trains may be unverified. Schedules, platforms, cancellations, and availability change frequently.

You must independently verify all train information with official and reliable railway sources — such as IRCTC (irctc.co.in), the National Train Enquiry System (NTES / enquiry.indianrail.gov.in), or the railway station enquiry — before booking, purchasing tickets, or travelling. The Service does not sell tickets and does not process bookings or payments for travel.

4. No warranties

The Service is provided "as is" and "as available", without warranties of any kind, express or implied, including accuracy, completeness, fitness for a particular purpose, or uninterrupted availability.

5. Limitation of liability

To the maximum extent permitted by law, shall not be liable for any direct, indirect, incidental, or consequential loss or damage — including missed trains or connections, additional costs, or losses arising from reliance on information shown on the Service. Your use of the Service is at your own risk.

6. Acceptable use

You agree not to misuse the Service, including: attempting to disrupt or overload it; scraping or harvesting data at scale; reverse-engineering or copying it; using it for unlawful purposes; or infringing the rights of others.

7. Accounts

If you create an account, you are responsible for the accuracy of your details and for keeping your credentials confidential and for all activity under your account. We may suspend or terminate accounts that violate these Terms.

8. Advertising

The Service displays advertisements, including from third-party advertising networks. Advertisements and any third-party content, products, or offers are the responsibility of the respective advertisers. We do not endorse and are not responsible for advertiser content or any dealings you have with advertisers.

9. Intellectual property

The Service's design, software, and original content are owned by and protected by applicable laws. You may not copy, reproduce, or redistribute them without permission. Third-party names and marks remain the property of their owners.

10. Third-party links

The Service may link to third-party websites and resources. We are not responsible for their content, availability, or practices.

11. Changes

We may modify the Service or these Terms at any time. Updated Terms will be posted on this page with a revised date. Continued use after changes take effect constitutes acceptance.

12. Governing law and jurisdiction

These Terms are governed by the laws of India. Subject to applicable law, the courts at shall have exclusive jurisdiction over any disputes arising from the Service.

13. Contact

Questions about these Terms: .

Privacy Policy · Disclaimer

-------------------- END OF FILE -------------------- FILE: train.php TYPE: PHP SIZE: 7.08 KB ------------------------------------------------------------ prepare("SELECT * FROM trains WHERE train_no = ?"); $tq->execute([$no]); $train = $tq->fetch(PDO::FETCH_ASSOC); if (!$train) { http_response_code(404); $page_title = 'Train ' . $no . ' not found'; $meta_robots = 'noindex'; require APP_DIR . '/header.php'; echo '

Train ' . e($no) . ' not found

' . '

We don\'t have this train number yet. ' . 'Search journeys instead.

'; require APP_DIR . '/footer.php'; exit; } $sq = $pdo->prepare( "SELECT s.seq, s.station_code, st.name AS sname, s.arrival, s.departure, s.day_offset, s.distance FROM train_stops s LEFT JOIN stations st ON st.code = s.station_code WHERE s.train_no = ? ORDER BY s.seq" ); $sq->execute([$no]); $stops = $sq->fetchAll(PDO::FETCH_ASSOC); $verified = !empty($train['run_days']); $nm = $train['train_name'] ?: ('Train ' . $no); $src = $train['source_code']; $dst = $train['dest_code']; $srcName = $stops[0]['sname'] ?? $src; $dstName = $stops ? ($stops[count($stops) - 1]['sname'] ?? $dst) : $dst; $daysTxt = rr_run_days_text($train['run_days'] ?? null); [$classLabel, $isUn] = rr_classify($no, $train['train_type'] ?? ''); $typeLabel = rr_type_label($train['train_type'] ?? ''); $nStops = count($stops); $totalKm = $stops ? (int) ($stops[count($stops) - 1]['distance'] ?? 0) : 0; $page_title = $no . ' ' . $nm . ' — Time Table & Route'; $meta_description = $no . ' ' . $nm . ' (' . $typeLabel . ') runs ' . $daysTxt . '. Schedule with arrival & departure times for ' . $nStops . ' stations from ' . $srcName . ' to ' . $dstName . '. Indicative times — verify before travel.'; $canonical = rr_abs(rr_train_url($no)); if (!$verified) $meta_robots = 'noindex'; $ld = [ '@context' => 'https://schema.org', '@type' => 'TrainTrip', 'trainNumber' => $no, 'trainName' => $nm, 'departureStation' => ['@type' => 'TrainStation', 'name' => $srcName, 'identifier' => $src], 'arrivalStation' => ['@type' => 'TrainStation', 'name' => $dstName, 'identifier' => $dst], ]; if ($stops) { if (!empty($stops[0]['departure'])) $ld['departureTime'] = substr($stops[0]['departure'], 0, 5); $last = $stops[count($stops) - 1]; if (!empty($last['arrival'])) $ld['arrivalTime'] = substr($last['arrival'], 0, 5); } $crumb = ['@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => [ ['@type' => 'ListItem', 'position' => 1, 'name' => 'Home', 'item' => rr_abs('/')], ['@type' => 'ListItem', 'position' => 2, 'name' => $no . ' ' . $nm, 'item' => $canonical], ]]; $head_extra = '' . ''; require APP_DIR . '/header.php'; ?>

·

Runs on
Stops
Distance
0 ? e($totalKm) . ' km' : '—' ?>
Booking
We're still confirming this train's timetable. Details below may be incomplete.

Schedule & route

$st): ?>
# Station Arr Dep Day Km
()

About this train

is a running between () and (). It operates and calls at stations.

Timings are indicative, based on a reference timetable. Always confirm on the official Indian Railways / IRCTC sources before booking or travel.

All trains

-------------------- END OF FILE -------------------- FILE: admin/ai_rundays.php TYPE: PHP SIZE: 6.33 KB ------------------------------------------------------------ 'Mon','MONDAY'=>'Mon','TUE'=>'Tue','TUES'=>'Tue','TUESDAY'=>'Tue','WED'=>'Wed','WEDNESDAY'=>'Wed', 'THU'=>'Thu','THUR'=>'Thu','THURS'=>'Thu','THURSDAY'=>'Thu','FRI'=>'Fri','FRIDAY'=>'Fri', 'SAT'=>'Sat','SATURDAY'=>'Sat','SUN'=>'Sun','SUNDAY'=>'Sun']; $ORDER = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; function rd_canon(array $list): array { global $DAYS, $ORDER; $set = []; foreach ($list as $d) { $k = strtoupper(trim((string)$d)); if (isset($DAYS[$k])) $set[$DAYS[$k]] = 1; } $out = []; foreach ($ORDER as $d) if (isset($set[$d])) $out[] = $d; return $out; } function openai_rundays(string $model, string $trainNo): array { $instructions = "You report Indian Railways train running days. Use web search to verify. " . "Reply with ONLY a JSON object (no prose, no code fences): " . '{"run_days":["Mon","Tue",...],"confidence":"high|medium|low","source":""}. ' . "run_days = weekday(s) the train departs origin. If not certain, set confidence low and run_days []. Never guess."; $body = json_encode([ 'model' => $model, 'tools' => [['type' => 'web_search']], 'instructions' => $instructions, 'input' => "Train number $trainNo — what days of the week does it currently run? Verify by searching.", ]); $ch = curl_init('https://api.openai.com/v1/responses'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY], CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 50, // return before nginx's ~60s, fail gracefully CURLOPT_SSL_VERIFYPEER => true, ]); $raw = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $cerr = curl_error($ch); curl_close($ch); if ($raw === false) return ['err' => 'Call failed/timed out: ' . $cerr]; if ($code !== 200) return ['err' => "HTTP $code: " . substr((string)$raw, 0, 200)]; $resp = json_decode($raw, true); $text = ''; if (isset($resp['output_text'])) { $text = $resp['output_text']; } elseif (isset($resp['output']) && is_array($resp['output'])) { foreach ($resp['output'] as $item) { if (($item['type'] ?? '') === 'message' && !empty($item['content'])) { foreach ($item['content'] as $c) if (($c['type'] ?? '') === 'output_text') $text .= $c['text'] ?? ''; } } } $text = trim(preg_replace('/^```(json)?|```$/m', '', (string)$text)); $json = json_decode($text, true); if (!is_array($json)) return ['err' => 'Unparseable reply: ' . substr($text, 0, 160)]; return ['days' => $json['run_days'] ?? [], 'conf' => (string)($json['confidence'] ?? '?'), 'src' => (string)($json['source'] ?? '')]; } $haveKey = defined('OPENAI_API_KEY') && OPENAI_API_KEY; /* ---- AJAX: test exactly one train, return JSON ---- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'one') { csrf_check(); header('Content-Type: application/json'); if (!$haveKey) { echo json_encode(['err' => 'OPENAI_API_KEY not set in app/secrets.php']); exit; } $tn = preg_replace('/\D/', '', (string)($_POST['train'] ?? '')); $model = trim((string)($_POST['model'] ?? '')) ?: $MODEL_DEFAULT; $exp = trim((string)($_POST['exp'] ?? '')); if ($tn === '') { echo json_encode(['err' => 'no train number']); exit; } $r = openai_rundays($model, $tn); if (isset($r['err'])) { echo json_encode(['err' => $r['err']]); exit; } $got = rd_canon($r['days']); $match = $exp === '' ? null : ($got === rd_canon(explode(',', $exp))); echo json_encode(['got' => $got, 'match' => $match, 'conf' => $r['conf'], 'src' => $r['src']]); exit; } $seed = "12244: Mon,Wed,Thu,Fri,Sat,Sun\n15607: Wed\n12512: Tue,Wed,Sun"; $page_title = 'AI running-days test'; require APP_DIR . '/header.php'; ?> ← Admin

AI running-days test

Asks OpenAI (with web search) for each train's running days and checks it against your verified answer. Trains are tested one at a time. One miss means the model isn't reliable enough.

Add your key to app/secrets.php: define('OPENAI_API_KEY', 'sk-...');
One line per train as number: Mon,Wed,Sun. Leave the days off to just see what the model returns. Verify the days in the free irctc1 playground first so the answer key is correct. Each train is one paid API call.
gpt-5.5 is strongest but slow; if rows time out, try gpt-5.4-mini.
-------------------- END OF FILE -------------------- FILE: admin/ai_vs_irctc.php TYPE: PHP SIZE: 10.05 KB ------------------------------------------------------------ 'Mon','MONDAY'=>'Mon','TUE'=>'Tue','TUES'=>'Tue','TUESDAY'=>'Tue','WED'=>'Wed','WEDNESDAY'=>'Wed', 'THU'=>'Thu','THUR'=>'Thu','THURS'=>'Thu','THURSDAY'=>'Thu','FRI'=>'Fri','FRIDAY'=>'Fri', 'SAT'=>'Sat','SATURDAY'=>'Sat','SUN'=>'Sun','SUNDAY'=>'Sun']; function rd_canon(array $list): array { global $DAYS, $ORDER; $set = []; foreach ($list as $d) { $k = strtoupper(trim((string)$d)); if (isset($DAYS[$k])) $set[$DAYS[$k]] = 1; } $out = []; foreach ($ORDER as $d) if (isset($set[$d])) $out[] = $d; return $out; } /* irctc run_days may be an array of day names, a 7-char Y/N or 1/0 string, or csv */ function irctc_days($rd): array { global $ORDER; if (is_array($rd)) { // could be ["Mon","Tue"] or [{"day":"Mon","runs":"Y"}] $flat = []; foreach ($rd as $x) { if (is_string($x)) $flat[] = $x; elseif (is_array($x)) { $name = $x['day'] ?? $x['name'] ?? ''; $r = $x['runs'] ?? $x['run'] ?? 'Y'; if ($name && ($r === 'Y' || $r === '1' || $r === true || $r === 1)) $flat[] = $name; } } return rd_canon($flat); } if (is_string($rd)) { $s = preg_replace('/\s+/', '', $rd); if (strlen($s) === 7 && preg_match('/^[YN01]{7}$/i', $s)) { $out = []; for ($i = 0; $i < 7; $i++) { $c = strtoupper($s[$i]); if ($c === 'Y' || $c === '1') $out[] = $ORDER[$i]; } return $out; } return rd_canon(explode(',', $rd)); } return []; } function parse_code(string $v): string { if (preg_match('/\(([A-Za-z0-9]{1,12})\)\s*$/', $v, $m)) return strtoupper($m[1]); return strtoupper(trim($v)); } function openai_rundays(string $model, string $trainNo): array { $instructions = "You report Indian Railways train running days. Use web search to verify. " . "Reply with ONLY a JSON object (no prose, no code fences): " . '{"run_days":["Mon","Tue",...],"confidence":"high|medium|low","source":""}. ' . "run_days = weekday(s) the train departs origin. If not certain, set confidence low and run_days []. Never guess."; $body = json_encode([ 'model' => $model, 'tools' => [['type' => 'web_search']], 'instructions' => $instructions, 'input' => "Train number $trainNo — what days of the week does it currently run? Verify by searching.", ]); $ch = curl_init('https://api.openai.com/v1/responses'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY], CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 50, CURLOPT_SSL_VERIFYPEER => true, ]); $raw = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $cerr = curl_error($ch); curl_close($ch); if ($raw === false) return ['err' => 'Call failed/timed out: ' . $cerr]; if ($code !== 200) return ['err' => "HTTP $code: " . substr((string)$raw, 0, 160)]; $resp = json_decode($raw, true); $text = ''; if (isset($resp['output_text'])) $text = $resp['output_text']; elseif (isset($resp['output']) && is_array($resp['output'])) { foreach ($resp['output'] as $item) if (($item['type'] ?? '') === 'message' && !empty($item['content'])) foreach ($item['content'] as $c) if (($c['type'] ?? '') === 'output_text') $text .= $c['text'] ?? ''; } $text = trim(preg_replace('/^```(json)?|```$/m', '', (string)$text)); $json = json_decode($text, true); if (!is_array($json)) return ['err' => 'Unparseable: ' . substr($text, 0, 140)]; return ['days' => rd_canon($json['run_days'] ?? []), 'conf' => (string)($json['confidence'] ?? '?'), 'src' => (string)($json['source'] ?? '')]; } function irctc_between(string $from, string $to, string $date): array { $url = 'https://irctc1.p.rapidapi.com/api/v3/trainBetweenStations?' . http_build_query(['fromStationCode' => $from, 'toStationCode' => $to, 'dateOfJourney' => $date]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['x-rapidapi-key: ' . RAPIDAPI_KEY, 'x-rapidapi-host: irctc1.p.rapidapi.com'], CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 40, CURLOPT_SSL_VERIFYPEER => true, ]); $raw = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $cerr = curl_error($ch); curl_close($ch); if ($raw === false) return ['err' => 'irctc network: ' . $cerr]; if ($code !== 200) return ['err' => "irctc HTTP $code: " . substr((string)$raw, 0, 200)]; $j = json_decode($raw, true); if (!is_array($j) || !isset($j['data']) || !is_array($j['data'])) return ['err' => 'irctc unexpected response: ' . substr((string)$raw, 0, 200)]; $out = []; foreach ($j['data'] as $t) { $tn = $t['train_number'] ?? $t['train_no'] ?? $t['trainNumber'] ?? null; if ($tn === null) continue; $out[] = [ 'tn' => (string)$tn, 'name' => (string)($t['train_name'] ?? $t['trainName'] ?? ''), 'days' => irctc_days($t['run_days'] ?? $t['runDays'] ?? $t['running_days'] ?? null), ]; } return ['trains' => $out]; } $haveRapid = defined('RAPIDAPI_KEY') && RAPIDAPI_KEY; $haveOpenAI = defined('OPENAI_API_KEY') && OPENAI_API_KEY; /* ---- AJAX 1: irctc truth for a route ---- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'irctc') { csrf_check(); header('Content-Type: application/json'); if (!$haveRapid) { echo json_encode(['err' => 'RAPIDAPI_KEY not set in app/secrets.php']); exit; } $from = parse_code((string)($_POST['from'] ?? '')); $to = parse_code((string)($_POST['to'] ?? '')); $date = (string)($_POST['date'] ?? ''); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) $date = date('Y-m-d', strtotime('+2 days')); if ($from === '' || $to === '') { echo json_encode(['err' => 'from/to required']); exit; } $res = irctc_between($from, $to, $date); if (isset($res['trains'])) { // attach each train's day_offset at the from-station (from our timetable) $off = db()->prepare('SELECT day_offset FROM train_stops WHERE train_no = ? AND station_code = ? ORDER BY seq LIMIT 1'); foreach ($res['trains'] as &$t) { $off->execute([$t['tn'], $from]); $row = $off->fetch(); $t['offset'] = $row ? (int)$row['day_offset'] : null; // null = train not in our data } unset($t); } echo json_encode($res); exit; } /* ---- AJAX 2: OpenAI for one train ---- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'one') { csrf_check(); header('Content-Type: application/json'); if (!$haveOpenAI) { echo json_encode(['err' => 'OPENAI_API_KEY not set in app/secrets.php']); exit; } $tn = preg_replace('/\D/', '', (string)($_POST['train'] ?? '')); $model = trim((string)($_POST['model'] ?? '')) ?: $MODEL_DEFAULT; if ($tn === '') { echo json_encode(['err' => 'no train']); exit; } $r = openai_rundays($model, $tn); echo json_encode(isset($r['err']) ? ['err' => $r['err']] : ['got' => $r['days'], 'conf' => $r['conf'], 'src' => $r['src']]); exit; } $page_title = 'irctc1 vs OpenAI'; require APP_DIR . '/header.php'; ?> ← Admin

irctc1 vs OpenAI — running days

One irctc1 call fetches the true running days for a whole route (the answer key), then OpenAI is tested per train against it. No hand-typed values.

Add define('RAPIDAPI_KEY', '...'); to app/secrets.php.
Add define('OPENAI_API_KEY', 'sk-...'); to app/secrets.php.
Pick a route with a mix of trains (some weekly ones make it a real test). Each train on the route = one paid OpenAI call, so cap it. irctc1 is treated as ground truth.
-------------------- END OF FILE -------------------- FILE: admin/index.php TYPE: PHP SIZE: 1.86 KB ------------------------------------------------------------ (int) db()->query('SELECT COUNT(*) FROM users')->fetchColumn(), 'admins' => (int) db()->query('SELECT COUNT(*) FROM users WHERE role="admin"')->fetchColumn(), 'trains' => (int) db()->query('SELECT COUNT(*) FROM trains')->fetchColumn(), 'stations' => (int) db()->query('SELECT COUNT(*) FROM stations')->fetchColumn(), 'verified' => (int) db()->query('SELECT COUNT(*) FROM trains WHERE run_days IS NOT NULL')->fetchColumn(), ]; $pending = max(0, $stats['trains'] - $stats['verified']); $page_title = 'Admin'; require APP_DIR . '/header.php'; ?>

Admin panel

Users

Admins

Trains

Stations

Verified (routing)

-------------------- END OF FILE -------------------- FILE: admin/report.php TYPE: PHP SIZE: 14.92 KB ------------------------------------------------------------ exec("CREATE TABLE IF NOT EXISTS analytics_events ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, ts INT UNSIGNED NOT NULL, day DATE NOT NULL, type VARCHAR(16) NOT NULL, sid CHAR(32) NOT NULL DEFAULT '', path VARCHAR(160) NOT NULL DEFAULT '', ref_host VARCHAR(120) NOT NULL DEFAULT '', src VARCHAR(40) NOT NULL DEFAULT '', device VARCHAR(8) NOT NULL DEFAULT '', bot TINYINT NOT NULL DEFAULT 0, from_code VARCHAR(10) NOT NULL DEFAULT '', to_code VARCHAR(10) NOT NULL DEFAULT '', train_no VARCHAR(10) NOT NULL DEFAULT '', jdate DATE NULL, n INT NOT NULL DEFAULT 0, note VARCHAR(60) NOT NULL DEFAULT '', ip_hash CHAR(16) NOT NULL DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); /* ---------------- filters ---------------- */ $today = new DateTime('now', new DateTimeZone('Asia/Kolkata')); $preset = $_GET['preset'] ?? '30'; $to = $_GET['to'] ?? $today->format('Y-m-d'); $from = $_GET['from'] ?? (clone $today)->modify('-29 day')->format('Y-m-d'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) $from = (clone $today)->modify('-29 day')->format('Y-m-d'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) $to = $today->format('Y-m-d'); if (isset($_GET['preset']) && !isset($_GET['from'])) { $to = $today->format('Y-m-d'); if ($preset === 'today') $from = $to; elseif ($preset === '7') $from = (clone $today)->modify('-6 day')->format('Y-m-d'); elseif ($preset === '30') $from = (clone $today)->modify('-29 day')->format('Y-m-d'); elseif ($preset === 'all') { $from = '2026-01-01'; } } $incbots = !empty($_GET['incbots']) ? 1 : 0; $dev = in_array($_GET['dev'] ?? '', ['mobile', 'desktop', 'bot'], true) ? $_GET['dev'] : ''; $srcF = isset($_GET['src']) ? substr(preg_replace('/[^0-9A-Za-z_\-]/', '', $_GET['src']), 0, 40) : ''; $w = ["day BETWEEN ? AND ?"]; $p = [$from, $to]; if (!$incbots) { $w[] = "bot = 0"; } if ($dev !== '') { $w[] = "device = ?"; $p[] = $dev; } if ($srcF !== '') { $w[] = "src = ?"; $p[] = $srcF; } $W = implode(' AND ', $w); function q(PDO $pdo, string $sql, array $p): array { $st = $pdo->prepare($sql); $st->execute($p); return $st->fetchAll(PDO::FETCH_ASSOC); } function names(PDO $pdo, string $table, string $keyCol, string $nameCol, array $keys): array { $keys = array_values(array_unique(array_filter($keys))); if (!$keys) return []; $in = implode(',', array_fill(0, count($keys), '?')); $st = $pdo->prepare("SELECT $keyCol k, $nameCol nm FROM $table WHERE $keyCol IN ($in)"); $st->execute($keys); $m = []; foreach ($st as $r) $m[$r['k']] = $r['nm']; return $m; } /* ---------------- metrics ---------------- */ $sum = q($pdo, "SELECT COALESCE(SUM(type='pageview'),0) pv, COUNT(DISTINCT CASE WHEN type='pageview' THEN sid END) uv, COALESCE(SUM(type='search'),0) s, COALESCE(SUM(type='availability'),0) a, COALESCE(SUM(type='pnr'),0) pnr FROM analytics_events WHERE $W", $p)[0] ?? ['pv'=>0,'uv'=>0,'s'=>0,'a'=>0,'pnr'=>0]; $daily = q($pdo, "SELECT day, COALESCE(SUM(type='pageview'),0) pv, COUNT(DISTINCT CASE WHEN type='pageview' THEN sid END) uv, COALESCE(SUM(type='search'),0) s, COALESCE(SUM(type='availability'),0) a, COALESCE(SUM(type='pnr'),0) pnr FROM analytics_events WHERE $W GROUP BY day ORDER BY day", $p); $routes = q($pdo, "SELECT from_code, to_code, COUNT(*) c FROM analytics_events WHERE $W AND type='search' AND from_code<>'' AND to_code<>'' GROUP BY from_code, to_code ORDER BY c DESC LIMIT 25", $p); $origins = q($pdo, "SELECT from_code k, COUNT(*) c FROM analytics_events WHERE $W AND type='search' AND from_code<>'' GROUP BY from_code ORDER BY c DESC LIMIT 12", $p); $dests = q($pdo, "SELECT to_code k, COUNT(*) c FROM analytics_events WHERE $W AND type='search' AND to_code<>'' GROUP BY to_code ORDER BY c DESC LIMIT 12", $p); $trains = q($pdo, "SELECT train_no k, COUNT(*) c FROM analytics_events WHERE $W AND type='availability' AND train_no<>'' GROUP BY train_no ORDER BY c DESC LIMIT 15", $p); $sources = q($pdo, "SELECT CASE WHEN src='' THEN '(direct / app)' ELSE src END src, COUNT(DISTINCT sid) uv, COUNT(*) hits FROM analytics_events WHERE $W AND type='pageview' GROUP BY src ORDER BY uv DESC LIMIT 15", $p); $referrers = q($pdo, "SELECT ref_host, COUNT(DISTINCT sid) uv FROM analytics_events WHERE $W AND type='pageview' AND ref_host<>'' GROUP BY ref_host ORDER BY uv DESC LIMIT 12", $p); $devices = q($pdo, "SELECT device, COUNT(DISTINCT sid) uv, COUNT(*) hits FROM analytics_events WHERE $W AND type='pageview' GROUP BY device ORDER BY uv DESC", $p); $stCodes = []; foreach ($routes as $r) { $stCodes[] = $r['from_code']; $stCodes[] = $r['to_code']; } foreach ($origins as $r) $stCodes[] = $r['k']; foreach ($dests as $r) $stCodes[] = $r['k']; $stName = names($pdo, 'stations', 'code', 'name', $stCodes); $trName = names($pdo, 'trains', 'train_no', 'train_name', array_column($trains, 'k')); function sn(array $m, string $c): string { return ($m[$c] ?? '') !== '' ? $m[$c] . " ($c)" : $c; } $maxPv = 1; foreach ($daily as $d) $maxPv = max($maxPv, (int) $d['pv']); $pvN = (int) $sum['pv']; $uvN = (int) $sum['uv']; $sN = (int) $sum['s']; $aN = (int) $sum['a']; $pnrN = (int) $sum['pnr']; $spv = $uvN ? round($sN / $uvN, 2) : 0; $aps = $sN ? round($aN / $sN * 100) : 0; function h($v): string { return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); } function qs(array $over): string { $base = ['from' => $_GET['from'] ?? null, 'to' => $_GET['to'] ?? null, 'preset' => $_GET['preset'] ?? null, 'incbots' => $_GET['incbots'] ?? null, 'dev' => $_GET['dev'] ?? null, 'src' => $_GET['src'] ?? null]; $m = array_merge($base, $over); $m = array_filter($m, fn($v) => $v !== null && $v !== ''); return '?' . http_build_query($m); } $page_title = 'Analytics'; require APP_DIR . '/header.php'; ?>

Analytics

Visitors
unique sessions
Pageviews
per visitor
Searches
per visitor
Availability checks
per 100 searches
PNR checks
this period

Daily trend — pageviews

No data in this range yet.

Day-by-day

DateVisitorsPageviewsSearchesAvail.PNR
No data.

Top routes searched

FromToSearches
No searches yet.

Top origin stations

StationSearches

Top destination stations

StationSearches

Most-checked trains (availability)

TrainChecks
No availability checks yet.

Traffic sources

SourceVisitorsViews

Tip: add ?src=wa to the URL in your WhatsApp message to see that campaign here.

Referrers & devices

ReferrerVisitors
— (direct/app)
DeviceVisitorsViews
-------------------- END OF FILE -------------------- FILE: admin/settings.php TYPE: PHP SIZE: 2.94 KB ------------------------------------------------------------ 60) $errors[] = 'Company name must be 1–60 characters.'; if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) $errors[] = 'Pick a valid colour.'; if (mb_strlen($tagline) > 160) $errors[] = 'Tagline too long.'; if (mb_strlen($footer) > 200) $errors[] = 'Footer note too long.'; if ($errors) { foreach ($errors as $err) flash($err, 'error'); } else { set_setting('company_name', $company); set_setting('tagline', $tagline); set_setting('footer_note', $footer); set_setting('brand_color', strtolower($color)); audit('settings_update', $company . ' / ' . $color, (int)$u['id']); flash('Settings saved. Branding and colours updated across the site.', 'success'); } redirect('/admin/settings.php'); } $page_title = 'Branding & theme'; require APP_DIR . '/header.php'; $presets = ['#0d6e6e','#1f6feb','#7b3fe4','#c2410c','#b91c1c','#0f766e','#15803d','#1f2937']; ?> ← Admin

Branding & theme

Renaming here updates the logo, page titles, footer and every mention automatically.

-------------------- END OF FILE -------------------- FILE: admin/users.php TYPE: PHP SIZE: 3.96 KB ------------------------------------------------------------ prepare('SELECT * FROM users WHERE id = ? LIMIT 1'); $st->execute([$target]); $tu = $st->fetch(); if (!$tu) { flash('User not found.', 'error'); redirect('/admin/users.php'); } switch ($action) { case 'make_admin': db()->prepare('UPDATE users SET role="admin" WHERE id=?')->execute([$target]); audit('grant_admin', $tu['email'], (int)$me['id']); flash('Granted admin to ' . $tu['email'], 'success'); break; case 'remove_admin': // Never allow removing the last admin. $admins = (int) db()->query('SELECT COUNT(*) FROM users WHERE role="admin"')->fetchColumn(); if ($admins <= 1) { flash('Cannot remove the last remaining admin.', 'error'); break; } db()->prepare('UPDATE users SET role="user" WHERE id=?')->execute([$target]); audit('revoke_admin', $tu['email'], (int)$me['id']); flash('Removed admin from ' . $tu['email'], 'success'); break; case 'block': db()->prepare('UPDATE users SET status="blocked" WHERE id=?')->execute([$target]); audit('block_user', $tu['email'], (int)$me['id']); flash('Blocked ' . $tu['email'], 'success'); break; case 'unblock': db()->prepare('UPDATE users SET status="active" WHERE id=?')->execute([$target]); audit('unblock_user', $tu['email'], (int)$me['id']); flash('Unblocked ' . $tu['email'], 'success'); break; } redirect('/admin/users.php'); } $users = db()->query('SELECT * FROM users ORDER BY created_at DESC LIMIT 500')->fetchAll(); $page_title = 'Users'; require APP_DIR . '/header.php'; ?> ← Admin

Users & admins

Promote any signed-up user to admin, or block accounts.

UserRoleStatusActions

Blocked' ?> (you)
-------------------- END OF FILE -------------------- FILE: admin/verify_queue.php TYPE: PHP SIZE: 7.41 KB ------------------------------------------------------------ 'Mon','tue'=>'Tue','wed'=>'Wed','thu'=>'Thu','fri'=>'Fri','sat'=>'Sat','sun'=>'Sun']; /* ---- Clear the router timetable cache so a newly-verified train shows up at once ---- */ function vq_clear_cache(): void { $f = sys_get_temp_dir() . '/trainchain_tt_v3.ser'; if (is_file($f)) @unlink($f); if (function_exists('apcu_delete')) @apcu_delete('tt_v3'); } /* ---- Handle save (POST), then redirect (PRG) ---- */ $flash = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['train_no'])) { $no = preg_replace('/\D/', '', (string) $_POST['train_no']); $days = (array) ($_POST['days'] ?? []); if ($no === '') { $flash = 'err:Invalid train number.'; } else { $mask = ''; foreach ($ORDER as $d) $mask .= in_array($d, $days, true) ? '1' : '0'; if (!preg_match('/^[01]{7}$/', $mask) || $mask === '0000000') { $flash = 'err:Pick at least one running day for train ' . $no . '.'; } else { $st = db()->prepare( "UPDATE trains SET run_days = ?, data_source = 'manual', refreshed_at = NOW() WHERE train_no = ?" ); $st->execute([$mask, $no]); vq_clear_cache(); $flash = 'ok:Train ' . $no . ' set to ' . $mask . ' and added to routing.'; } } $qs = http_build_query(array_filter([ 'f' => $_GET['f'] ?? null, 'q' => $_GET['q'] ?? null, 'p' => $_GET['p'] ?? null, ])); header('Location: /admin/verify_queue.php' . ($qs ? '?' . $qs : '') . '#m' . $no); // pass flash via session so it survives the redirect $_SESSION['vq_flash'] = $flash; exit; } if (!empty($_SESSION['vq_flash'])) { $flash = $_SESSION['vq_flash']; unset($_SESSION['vq_flash']); } /* ---- Filters / paging ---- */ $f = $_GET['f'] ?? 'untried'; // untried | noschedule | all $q = trim((string) ($_GET['q'] ?? '')); $page = max(1, (int) ($_GET['p'] ?? 1)); $per = 50; $off = ($page - 1) * $per; $where = ['run_days IS NULL']; $args = []; if ($f === 'untried') $where[] = 'data_source IS NULL'; elseif ($f === 'noschedule') $where[] = "data_source = 'no_schedule'"; if ($q !== '') { $where[] = '(train_no LIKE ? OR train_name LIKE ?)'; $args[] = "%$q%"; $args[] = "%$q%"; } $wsql = implode(' AND ', $where); $total = (int) (function () use ($wsql, $args) { $s = db()->prepare("SELECT COUNT(*) FROM trains WHERE $wsql"); $s->execute($args); return $s->fetchColumn(); })(); $pages = max(1, (int) ceil($total / $per)); $rows = (function () use ($wsql, $args, $per, $off) { $s = db()->prepare( "SELECT train_no, train_name, source_code, dest_code, data_source FROM trains WHERE $wsql ORDER BY train_no LIMIT $per OFFSET $off" ); $s->execute($args); return $s->fetchAll(); })(); /* ---- Counters for the header ---- */ $c = db()->query( "SELECT SUM(run_days IS NOT NULL) AS verified, SUM(run_days IS NULL AND data_source IS NULL) AS untried, SUM(data_source = 'no_schedule') AS noschedule, COUNT(*) AS total FROM trains" )->fetch(); $page_title = 'Verify queue'; require APP_DIR . '/header.php'; ?>

Pending verification

Only trains with a verified running-days mask appear in journey results. Commit a mask below to add a train to routing.

Verified (routing)

Untried

No API schedule

Total trains

Untried () No API schedule () All unverified

Tip: use AI run-days to look up a train's days, then tick them here and Save.

Nothing pending in this view. 🎉

·
1): ?>
$f,'q'=>$q]); ?> 1): ?>← Prev Page of · trains Next →
-------------------- END OF FILE -------------------- FILE: app/ad_relevantreflex.php TYPE: PHP SIZE: 1.47 KB ------------------------------------------------------------ */ ?> -------------------- END OF FILE -------------------- FILE: app/analytics.php TYPE: PHP SIZE: 5.03 KB ------------------------------------------------------------ exec("CREATE TABLE IF NOT EXISTS analytics_events ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, ts INT UNSIGNED NOT NULL, day DATE NOT NULL, type VARCHAR(16) NOT NULL, sid CHAR(32) NOT NULL DEFAULT '', path VARCHAR(160) NOT NULL DEFAULT '', ref_host VARCHAR(120) NOT NULL DEFAULT '', src VARCHAR(40) NOT NULL DEFAULT '', device VARCHAR(8) NOT NULL DEFAULT '', bot TINYINT NOT NULL DEFAULT 0, from_code VARCHAR(10) NOT NULL DEFAULT '', to_code VARCHAR(10) NOT NULL DEFAULT '', train_no VARCHAR(10) NOT NULL DEFAULT '', jdate DATE NULL, n INT NOT NULL DEFAULT 0, note VARCHAR(60) NOT NULL DEFAULT '', ip_hash CHAR(16) NOT NULL DEFAULT '', KEY k_day (day), KEY k_type (type), KEY k_sid (sid), KEY k_from (from_code), KEY k_to (to_code), KEY k_train (train_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $done = true; } function rr_session_id(): string { static $sid = null; if ($sid !== null) return $sid; $c = $_COOKIE['rr_sid'] ?? ''; if (preg_match('/^[a-f0-9]{32}$/', $c)) { $sid = $c; return $sid; } try { $sid = bin2hex(random_bytes(16)); } catch (Throwable $e) { $sid = md5(uniqid('', true)); } if (!headers_sent()) { $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); @setcookie('rr_sid', $sid, [ 'expires' => time() + 31536000, 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); } $_COOKIE['rr_sid'] = $sid; return $sid; } function rr_is_bot(string $ua): bool { return $ua === '' || (bool) preg_match( '/(bot|crawl|spider|slurp|bing|google|yandex|baidu|duckduck|facebookexternal|whatsapp|telegram|embed|preview|monitor|curl|wget|python-requests|headless|lighthouse)/i', $ua ); } function rr_device(string $ua): string { if (rr_is_bot($ua)) return 'bot'; return preg_match('/Mobi|Android|iPhone|iPad|iPod|Windows Phone/i', $ua) ? 'mobile' : 'desktop'; } /** * Log one event. $type = pageview | search | availability. * $d (all optional): path, src, from_code, to_code, train_no, jdate, n, note, force * The admin's own browsing is skipped (unless force) so visitor counts stay honest. */ function rr_track(string $type, array $d = []): void { try { if (empty($d['force']) && function_exists('current_user')) { $u = current_user(); if ($u && (($u['role'] ?? '') === 'admin')) return; } $pdo = db(); rr_analytics_ready($pdo); $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $ref = $_SERVER['HTTP_REFERER'] ?? ''; $rh = $ref !== '' ? (string) (parse_url($ref, PHP_URL_HOST) ?: '') : ''; $self = (string) ($_SERVER['HTTP_HOST'] ?? ''); if ($rh !== '' && $self !== '' && stripos($rh, $self) !== false) $rh = ''; // ignore internal referrers $src = (string) ($d['src'] ?? $_GET['src'] ?? $_GET['utm_source'] ?? ''); $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip)[0]); $jdate = (!empty($d['jdate']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $d['jdate'])) ? $d['jdate'] : null; $st = $pdo->prepare("INSERT INTO analytics_events (ts, day, type, sid, path, ref_host, src, device, bot, from_code, to_code, train_no, jdate, n, note, ip_hash) VALUES (?,CURDATE(),?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); $st->execute([ time(), substr($type, 0, 16), rr_session_id(), substr((string) ($d['path'] ?? ($_SERVER['REQUEST_URI'] ?? '')), 0, 160), substr($rh, 0, 120), substr($src, 0, 40), rr_device($ua), rr_is_bot($ua) ? 1 : 0, strtoupper(substr((string) ($d['from_code'] ?? ''), 0, 10)), strtoupper(substr((string) ($d['to_code'] ?? ''), 0, 10)), preg_replace('/[^0-9]/', '', (string) ($d['train_no'] ?? '')), $jdate, (int) ($d['n'] ?? 0), substr((string) ($d['note'] ?? ''), 0, 60), $ip !== '' ? substr(hash('sha256', $ip . RR_ANALYTICS_SALT), 0, 16) : '', ]); } catch (Throwable $e) { /* analytics must never break the page */ } } -------------------- END OF FILE -------------------- FILE: app/auth.php TYPE: PHP SIZE: 5.53 KB ------------------------------------------------------------ 0, 'path' => '/', 'secure' => $https, 'httponly' => true, 'samesite' => 'Lax', ]); ini_set('session.use_strict_mode', '1'); session_start(); } /* ---------- Current user ---------- */ function current_user(): ?array { if (empty($_SESSION['uid'])) return null; static $u = null; if ($u !== null) return $u ?: null; $st = db()->prepare('SELECT * FROM users WHERE id = ? AND status = "active" LIMIT 1'); $st->execute([$_SESSION['uid']]); $u = $st->fetch() ?: false; if (!$u) { logout_user(); return null; } return $u; } function is_logged_in(): bool { return current_user() !== null; } function is_admin(): bool { $u = current_user(); return $u && $u['role'] === 'admin'; } function require_login(): void { if (!is_logged_in()) { flash('Please sign in to continue.'); redirect('/login.php'); } } function require_admin(): void { require_login(); if (!is_admin()) { http_response_code(403); exit('Forbidden.'); } } /* ---------- Login / logout ---------- */ function login_user(array $user): void { session_regenerate_id(true); // prevent session fixation $_SESSION['uid'] = (int)$user['id']; db()->prepare('UPDATE users SET last_login_at = NOW() WHERE id = ?') ->execute([$user['id']]); audit('login', $user['email'], (int)$user['id']); } function logout_user(): void { $_SESSION = []; if (ini_get('session.use_cookies')) { $p = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']); } session_destroy(); } /* ---------- Google OAuth 2.0 (dependency-free) ---------- */ function google_auth_url(): string { $_SESSION['oauth_state'] = bin2hex(random_bytes(16)); $params = [ 'client_id' => GOOGLE_CLIENT_ID, 'redirect_uri' => BASE_URL . '/oauth-callback.php', 'response_type' => 'code', 'scope' => 'openid email profile', 'state' => $_SESSION['oauth_state'], 'access_type' => 'online', 'prompt' => 'select_account', ]; return 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query($params); } /** Exchange code for tokens, then fetch the verified profile. */ function google_fetch_profile(string $code): ?array { // 1) code -> tokens $tok = http_post('https://oauth2.googleapis.com/token', [ 'code' => $code, 'client_id' => GOOGLE_CLIENT_ID, 'client_secret' => GOOGLE_CLIENT_SECRET, 'redirect_uri' => BASE_URL . '/oauth-callback.php', 'grant_type' => 'authorization_code', ]); if (!$tok || empty($tok['access_token'])) return null; // 2) tokens -> userinfo $info = http_get('https://openidconnect.googleapis.com/v1/userinfo', $tok['access_token']); if (!$info || empty($info['sub']) || empty($info['email'])) return null; if (isset($info['email_verified']) && $info['email_verified'] === false) return null; return $info; } /** Create or update the user, applying first-admin bootstrap. */ function upsert_google_user(array $p): array { $pdo = db(); $st = $pdo->prepare('SELECT * FROM users WHERE google_id = ? OR email = ? LIMIT 1'); $st->execute([$p['sub'], $p['email']]); $existing = $st->fetch(); $name = $p['name'] ?? ''; $pic = $p['picture'] ?? ''; if ($existing) { $pdo->prepare('UPDATE users SET google_id=?, name=?, picture=? WHERE id=?') ->execute([$p['sub'], $name, $pic, $existing['id']]); $st->execute([$p['sub'], $p['email']]); return $st->fetch(); } // New user. Bootstrap admin if this is the configured first-admin email. $role = (strtolower($p['email']) === strtolower(BOOTSTRAP_ADMIN_EMAIL)) ? 'admin' : 'user'; $ins = $pdo->prepare( 'INSERT INTO users (google_id, email, name, picture, role) VALUES (?,?,?,?,?)'); $ins->execute([$p['sub'], $p['email'], $name, $pic, $role]); $id = (int)$pdo->lastInsertId(); audit('signup', $p['email'] . ($role === 'admin' ? ' (bootstrap admin)' : ''), $id); $st->execute([$p['sub'], $p['email']]); return $st->fetch(); } /* ---------- Minimal HTTP helpers (cURL) ---------- */ function http_post(string $url, array $data): ?array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $res = curl_exit_json($ch); return $res; } function http_get(string $url, string $bearer): ?array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $bearer], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); return curl_exit_json($ch); } function curl_exit_json($ch): ?array { $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; $j = json_decode($body, true); return is_array($j) ? $j : null; } -------------------- END OF FILE -------------------- FILE: app/config.php TYPE: PHP SIZE: 1.82 KB ------------------------------------------------------------ 'HH:MM'; null/empty -> em dash. */ function rr_time(?string $t): string { if ($t === null || $t === '') return '—'; return substr($t, 0, 5); } /** 7-char Mon..Sun mask -> readable text. */ function rr_run_days_text(?string $mask): string { if (!$mask || strlen($mask) < 7) return 'Running days not confirmed'; if ($mask === '1111111') return 'Daily'; $names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; $out = []; for ($i = 0; $i < 7; $i++) if ($mask[$i] === '1') $out[] = $names[$i]; return $out ? implode(', ', $out) : 'Running days not confirmed'; } /** Returns [human label, isUnreserved bool] using train_type text + IR numbering heuristic. */ function rr_classify(string $no, ?string $type): array { $t = strtoupper(trim((string) $type)); $un = false; foreach (['SUBURBAN', 'SUB', 'PASSENGER', 'PASS', 'MEX', 'DEMU', 'MEMU', 'MULTIPLE UNIT', 'EMU'] as $tok) { if ($t !== '' && strpos($t, $tok) !== false) { $un = true; break; } } if (!$un) { $d = $no[0] ?? ''; $reservedToken = ($t !== '' && ( strpos($t, 'EXP') !== false || strpos($t, 'SUPERFAST') !== false || strpos($t, 'MAIL') !== false || strpos($t, 'RAJ') !== false || strpos($t, 'SHAT') !== false || strpos($t, 'DURONTO') !== false || strpos($t, 'VANDE') !== false || strpos($t, 'GARIB') !== false || strpos($t, 'JAN') !== false)); if ($d !== '' && strpos('345679', $d) !== false && !$reservedToken) $un = true; } $label = $un ? 'Unreserved (general / platform ticket)' : 'Reserved (online booking available)'; return [$label, $un]; } /** Pretty train-type label. */ function rr_type_label(?string $type): string { $t = strtoupper(trim((string) $type)); if ($t === '') return 'Train'; $map = [ 'SUPERFAST' => 'Superfast Express', 'MAIL EXPRESS' => 'Mail/Express', 'RAJDHANI' => 'Rajdhani Express', 'SHATABDI' => 'Shatabdi Express', 'DURONTO' => 'Duronto Express', 'VANDE BHARAT' => 'Vande Bharat', 'GARIB RATH' => 'Garib Rath', 'JANSHATABDI' => 'Jan Shatabdi', 'JAN SHATABDI' => 'Jan Shatabdi', 'PASSENGER' => 'Passenger', 'MEMU' => 'MEMU', 'DEMU' => 'DEMU', 'SUBURBAN' => 'Suburban / Local', 'EXPRESS' => 'Express', 'MEX' => 'Mail/Express', 'EXP' => 'Express', 'SF' => 'Superfast Express', 'PASS' => 'Passenger', 'SUB' => 'Suburban / Local', ]; foreach ($map as $k => $v) if (strpos($t, $k) !== false) return $v; return ucwords(strtolower($t)); } -------------------- END OF FILE -------------------- FILE: app/db.php TYPE: PHP SIZE: 824 B ------------------------------------------------------------ PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, // true server-side prepares PDO::ATTR_STRINGIFY_FETCHES => false, ]; try { $pdo = new PDO($dsn, DB_USER, DB_PASS, $opt); } catch (Throwable $e) { error_log('DB connect failed: ' . $e->getMessage()); http_response_code(500); exit('Service temporarily unavailable.'); } return $pdo; } -------------------- END OF FILE -------------------- FILE: app/footer.php TYPE: PHP SIZE: 1.27 KB ------------------------------------------------------------
-------------------- END OF FILE -------------------- FILE: app/functions.php TYPE: PHP SIZE: 4.59 KB ------------------------------------------------------------ $msg, 't' => $type]; } function take_flash(): array { $f = $_SESSION['flash'] ?? []; unset($_SESSION['flash']); return $f; } /* ---------- CSRF ---------- */ function csrf_token(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } function csrf_field(): string { return ''; } function csrf_check(): void { $ok = isset($_POST['csrf'], $_SESSION['csrf']) && hash_equals($_SESSION['csrf'], (string)$_POST['csrf']); if (!$ok) { http_response_code(419); exit('Session expired or invalid request. Please go back and try again.'); } } /* ---------- Settings (cached per request) ---------- */ function settings(): array { static $cache = null; if ($cache !== null) return $cache; $cache = []; foreach (db()->query('SELECT skey, svalue FROM settings') as $r) { $cache[$r['skey']] = $r['svalue']; } return $cache; } function setting(string $key, string $default = ''): string { $s = settings(); return $s[$key] ?? $default; } function set_setting(string $key, string $value): void { $st = db()->prepare( 'INSERT INTO settings (skey, svalue) VALUES (?, ?) ON DUPLICATE KEY UPDATE svalue = VALUES(svalue)'); $st->execute([$key, $value]); } /* ---------- Single-colour theme palette ---------- * One brand hex drives the whole UI. We derive tints/shades and pick a * readable text colour by luminance. Output as CSS custom properties. */ function hex_to_rgb(string $hex): array { $hex = ltrim($hex, '#'); if (strlen($hex) === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) $hex = '0d6e6e'; // safe fallback return [hexdec(substr($hex,0,2)), hexdec(substr($hex,2,2)), hexdec(substr($hex,4,2))]; } function rgb_to_hex(array $c): string { return sprintf('#%02x%02x%02x', max(0,min(255,(int)round($c[0]))), max(0,min(255,(int)round($c[1]))), max(0,min(255,(int)round($c[2])))); } /** mix toward white (pct>0) or black (pct<0); pct in -100..100 */ function shade(string $hex, int $pct): string { [$r,$g,$b] = hex_to_rgb($hex); $t = $pct >= 0 ? 255 : 0; $p = abs($pct) / 100; return rgb_to_hex([ $r + ($t - $r) * $p, $g + ($t - $g) * $p, $b + ($t - $b) * $p, ]); } /** white or near-black text for best contrast on a given colour */ function on_color(string $hex): string { [$r,$g,$b] = hex_to_rgb($hex); $lum = (0.2126*$r + 0.7152*$g + 0.0722*$b) / 255; return $lum > 0.6 ? '#10231f' : '#ffffff'; } function theme_css(): string { $brand = setting('brand_color', '#0d6e6e'); if (!preg_match('/^#?[0-9a-fA-F]{3,6}$/', $brand)) $brand = '#0d6e6e'; $vars = [ '--brand' => $brand, '--brand-700' => shade($brand, -22), '--brand-600' => shade($brand, -10), '--brand-300' => shade($brand, 45), '--brand-100' => shade($brand, 86), '--brand-50' => shade($brand, 94), '--on-brand' => on_color($brand), ]; $out = ':root{'; foreach ($vars as $k => $v) $out .= $k . ':' . $v . ';'; $out .= '}'; return $out; } /* ---------- Audit ---------- */ function audit(string $action, string $detail = '', ?int $uid = null): void { try { $st = db()->prepare('INSERT INTO audit_log (user_id, action, detail, ip) VALUES (?,?,?,?)'); $st->execute([$uid, $action, mb_substr($detail,0,255), $_SERVER['REMOTE_ADDR'] ?? '']); } catch (Throwable $e) { /* never block on logging */ } } /* ---------- Time helpers for schedules ---------- */ function fmt_time(?string $t): string { if (!$t) return '—'; return date('H:i', strtotime($t)); } function fmt_duration(?int $minutes): string { if ($minutes === null || $minutes < 0) return '—'; return intdiv($minutes,60) . 'h ' . str_pad((string)($minutes%60),2,'0',STR_PAD_LEFT) . 'm'; } -------------------- END OF FILE -------------------- FILE: app/header.php TYPE: PHP SIZE: 4.12 KB ------------------------------------------------------------ <?= e($title) ?>
-------------------- END OF FILE -------------------- FILE: app/router.php TYPE: PHP SIZE: 11.22 KB ------------------------------------------------------------ [sid, arrMin, depMin, depTod, dayOff] $cur = null; $tid = -1; $flat = []; // When verified-only is on, the graph contains ONLY trains with a run_days mask; // unverified trains (untried or no_schedule) are excluded from routing entirely. $verifiedOnly = defined('RT_VERIFIED_ONLY') ? RT_VERIFIED_ONLY : true; try { if ($verifiedOnly) { $stmt = $db->query( 'SELECT ts.train_no, ts.station_code, ts.arr_min, ts.dep_min, ts.departure, ts.day_offset FROM train_stops ts JOIN trains t ON t.train_no = ts.train_no WHERE t.run_days IS NOT NULL ORDER BY ts.train_no, ts.seq' ); } else { $stmt = $db->query( 'SELECT train_no, station_code, arr_min, dep_min, departure, day_offset FROM train_stops ORDER BY train_no, seq' ); } } catch (Throwable $e) { // older schema without run_days -> fall back to all trains $stmt = $db->query( 'SELECT train_no, station_code, arr_min, dep_min, departure, day_offset FROM train_stops ORDER BY train_no, seq' ); } $flush = function() use (&$trips, &$tid, &$flat) { if ($tid >= 0) $trips[$tid] = $flat; }; foreach ($stmt as $row) { if ($row['train_no'] !== $cur) { $flush(); $cur = $row['train_no']; $tid = count($trainNo); $trainNo[] = $cur; $flat = []; } $code = $row['station_code']; if (!isset($stId[$code])) { $stId[$code] = count($stRev); $stRev[] = $code; } $sid = $stId[$code]; $arrMin = $row['arr_min'] === null ? -1 : (int)$row['arr_min']; $depMin = $row['dep_min'] === null ? -1 : (int)$row['dep_min']; $depTod = $row['departure'] === null ? -1 : rt_tod($row['departure']); $dayOff = (int)$row['day_offset']; $idx = intdiv(count($flat), 5); $flat[] = $sid; $flat[] = $arrMin; $flat[] = $depMin; $flat[] = $depTod; $flat[] = $dayOff; if ($depTod >= 0) { $dep[$sid][] = $tid; $dep[$sid][] = $idx; } } $flush(); // Per-train run-day mask + freshness, keyed to tid via train number. $noToTid = array_flip($trainNo); // train_no => tid $runmask = array_fill(0, count($trainNo), null); // '1111111' (Mon..Sun) or null = unharvested $fresh = array_fill(0, count($trainNo), 0); // unix ts of refreshed_at, 0 = never // run_days / refreshed_at live on trains; tolerate older schemas without them. try { $q = $db->query('SELECT train_no, run_days, refreshed_at FROM trains'); foreach ($q as $r) { $tn = $r['train_no']; if (!isset($noToTid[$tn])) continue; $i = $noToTid[$tn]; $m = $r['run_days'] ?? null; if (is_string($m) && preg_match('/^[01]{7}$/', $m)) $runmask[$i] = $m; $fresh[$i] = !empty($r['refreshed_at']) ? (int)strtotime($r['refreshed_at']) : 0; } } catch (Throwable $e) { /* columns not present yet -> everything stays daily */ } return ['stId'=>$stId, 'stRev'=>$stRev, 'trainNo'=>$trainNo, 'trips'=>$trips, 'dep'=>$dep, 'runmask'=>$runmask, 'fresh'=>$fresh]; } function rt_tod(?string $time): int { if ($time === null || $time === '') return -1; $p = explode(':', $time); return count($p) >= 2 ? ((int)$p[0]) * 60 + (int)$p[1] : -1; } /* next absolute minute >= E whose time-of-day == tod (daily instances) */ function rt_next(int $tod, int $E): int { $d = (($tod - $E) % 1440 + 1440) % 1440; return $E + $d; } /* Does this train run for a boarding at $depAbs? mask null => assume daily (true). * baseDow = ISO weekday (1=Mon..7=Sun) of the SEARCH date (midnight = abs minute 0). * dayOff = boarding stop's day_offset (calendar days after the train's origin dep). * Origin departure weekday = searchDOW + floor(depAbs/1440) - dayOff (mod 7). */ function rt_runs(?string $mask, int $baseDow, int $depAbs, int $dayOff): bool { if ($mask === null) return true; $boardDayIdx = intdiv($depAbs, 1440); $idx = (($baseDow - 1) + $boardDayIdx - $dayOff) % 7; $idx = ($idx + 7) % 7; // Mon=0 .. Sun=6 return $mask[$idx] === '1'; } /* First daily instance >= earliest, within window, on a day the train ACTUALLY runs. -1 if none. */ function rt_first_run(?string $mask, int $baseDow, int $dayOff, int $depTod, int $earliest, int $window): int { $start = rt_next($depTod, $earliest); for ($cand = $start; $cand <= $earliest + $window; $cand += 1440) { if (rt_runs($mask, $baseDow, $cand, $dayOff)) return $cand; } return -1; } function rt_plan(array $tt, string $fromCode, string $toCode, int $queryDep, array $opt = []): array { $minT = $opt['min_transfer'] ?? RT_MIN_TRANSFER; $maxL = $opt['max_layover'] ?? RT_MAX_LAYOVER; $hor = $opt['horizon'] ?? RT_HORIZON; $ymd = $opt['date'] ?? date('Y-m-d'); $baseDow = (int)date('N', strtotime($ymd)); // 1=Mon..7=Sun if (!isset($tt['stId'][$fromCode], $tt['stId'][$toCode])) return ['direct'=>[], 'hops'=>[]]; $from = $tt['stId'][$fromCode]; $to = $tt['stId'][$toCode]; $trips = $tt['trips']; $dep = $tt['dep']; $runmask = $tt['runmask'] ?? []; $direct = rt_direct($tt, $from, $to, $queryDep, $hor, $baseDow); if (!isset($dep[$from])) return ['direct'=>rt_label_journey($tt, $direct), 'hops'=>[]]; $arr = [0 => [$from => $queryDep]]; $pred = [0 => []]; $marked = [$from => $queryDep]; for ($r = 1; $r <= RT_MAX_ROUNDS; $r++) { $arr[$r] = $arr[$r-1]; $pred[$r] = $pred[$r-1]; $ridden = []; $newMarked = []; foreach ($marked as $s => $a) { if (!isset($dep[$s])) continue; $earliest = ($r === 1) ? $a : $a + $minT; $window = ($r === 1) ? $hor : $maxL; $arrivedOn = ($r > 1 && isset($pred[$r-1][$s])) ? $pred[$r-1][$s]['train'] : -1; $d = $dep[$s]; $dn = count($d); for ($e = 0; $e < $dn; $e += 2) { $tid = $d[$e]; $idx = $d[$e+1]; if ($tid === $arrivedOn) continue; $trip = $trips[$tid]; $off = $idx * 5; $depTod = $trip[$off+3]; $depMin = $trip[$off+2]; $dayOff = $trip[$off+4]; $mask = $runmask[$tid] ?? null; $depAbs = rt_first_run($mask, $baseDow, $dayOff, $depTod, $earliest, $window); if ($depAbs < 0) continue; // doesn't run within the window if (isset($ridden[$tid]) && $depAbs >= $ridden[$tid]) continue; $ridden[$tid] = $depAbs; $bestDest = $arr[$r][$to] ?? RT_INF; $m = count($trip); for ($j = $off + 5; $j < $m; $j += 5) { $arrMin = $trip[$j+1]; if ($arrMin < 0) continue; $arrAbs = $depAbs + ($arrMin - $depMin); if ($arrAbs >= $bestDest) continue; $code2 = $trip[$j]; if ($arrAbs < ($arr[$r][$code2] ?? RT_INF)) { $arr[$r][$code2] = $arrAbs; $pred[$r][$code2] = ['from'=>$s, 'train'=>$tid, 'board'=>$depAbs, 'arr'=>$arrAbs]; $newMarked[$code2] = $arrAbs; } } } } $marked = $newMarked; if (!$marked) break; } $hops = []; for ($r = 1; $r <= RT_MAX_ROUNDS; $r++) { if (!isset($arr[$r][$to])) continue; if ($r > 1 && isset($arr[$r-1][$to]) && $arr[$r][$to] >= $arr[$r-1][$to]) continue; $journey = rt_reconstruct($pred, $r, $from, $to); $changes = count($journey) - 1; if ($changes >= 1) $hops[$changes] = rt_label_journey($tt, $journey); } return ['direct'=>rt_label_journey($tt, $direct), 'hops'=>$hops]; } function rt_reconstruct(array $pred, int $R, int $from, int $to): array { $legs = []; $code = $to; $r = $R; $guard = 0; while ($code !== $from && $r >= 1 && $guard++ < 12) { if (!isset($pred[$r][$code])) break; $leg = $pred[$r][$code]; $legs[] = ['from'=>$leg['from'], 'to'=>$code, 'train'=>$leg['train'], 'board'=>$leg['board'], 'arr'=>$leg['arr']]; $code = $leg['from']; $r--; } return array_reverse($legs); } function rt_direct(array $tt, int $from, int $to, int $queryDep, int $hor, int $baseDow): array { $out = []; if (!isset($tt['dep'][$from])) return $out; $runmask = $tt['runmask'] ?? []; $d = $tt['dep'][$from]; $dn = count($d); for ($e = 0; $e < $dn; $e += 2) { $tid = $d[$e]; $idx = $d[$e+1]; $trip = $tt['trips'][$tid]; $off = $idx*5; $m = count($trip); $depTod = $trip[$off+3]; $depMin = $trip[$off+2]; $dayOff = $trip[$off+4]; $mask = $runmask[$tid] ?? null; for ($j = $off+5; $j < $m; $j += 5) { if ($trip[$j] !== $to || $trip[$j+1] < 0) continue; $depAbs = rt_first_run($mask, $baseDow, $dayOff, $depTod, $queryDep, $hor); if ($depAbs < 0) break; // doesn't run in window $arrAbs = $depAbs + ($trip[$j+1] - $depMin); $out[] = ['from'=>$from, 'to'=>$to, 'train'=>$tid, 'board'=>$depAbs, 'arr'=>$arrAbs]; break; } } usort($out, fn($a,$b) => $a['arr'] <=> $b['arr']); return $out; } /* map ids back to codes / train numbers, and attach run-day label + freshness */ function rt_label_journey(array $tt, array $legs): array { $now = time(); foreach ($legs as &$l) { $tid = $l['train']; $mask = $tt['runmask'][$tid] ?? null; $ts = $tt['fresh'][$tid] ?? 0; $l['from'] = $tt['stRev'][$l['from']]; $l['to'] = $tt['stRev'][$l['to']]; $l['train'] = $tt['trainNo'][$tid]; $l['days'] = $mask === null ? null : (function_exists('rd_label') ? rd_label($mask) : $mask); $l['harvested'] = $mask !== null; $l['stale'] = $mask !== null && ($ts === 0 || $ts < $now - RT_STALE_DAYS*86400); } return $legs; } -------------------- END OF FILE -------------------- FILE: app/running_days.php TYPE: PHP SIZE: 7.21 KB ------------------------------------------------------------ 0,'tue'=>1,'wed'=>2,'thu'=>3,'fri'=>4,'sat'=>5,'sun'=>6]; // "... except Sunday" style → start full, remove named days if (strpos($s, 'except') !== false) { $d = [1,1,1,1,1,1,1]; foreach ($map as $a=>$i) if (strpos($s,$a)!==false) $d[$i]=0; return $d; } // Explicit daily if (preg_match('/\b(daily|every ?day|all ?7|7 ?days)\b/', $s)) return [1,1,1,1,1,1,1]; // Day-name list (order-independent; works for "Mon,Wed,Fri", full names, etc.) $d = [0,0,0,0,0,0,0]; $found = false; foreach ($map as $a=>$i) if (strpos($s,$a)!==false) { $d[$i]=1; $found=true; } if ($found) return $d; // "weekly"/"bi-weekly" with no day named → cannot determine pattern return null; } function rd_to_string(?array $d): ?string { return $d === null ? null : implode('', array_map(fn($x)=>$x?1:0, $d)); } /* Pure consensus vote. Input: [sourceName => "1111111"|null]. * Returns [pattern|null, confidence]. confidence: high|medium|low|none. */ function rd_vote(array $perSource): array { $counts = []; foreach ($perSource as $patt) { if ($patt !== null) $counts[$patt] = ($counts[$patt] ?? 0) + 1; } if (!$counts) return [null, 'none']; arsort($counts); $pattern = array_key_first($counts); $usable = array_sum($counts); $distinct = count($counts); if ($distinct === 1 && $usable >= 3) $confidence = 'high'; elseif ($distinct === 1 && $usable == 2) $confidence = 'medium'; elseif ($distinct === 1 && $usable == 1) $confidence = 'low'; // single source only else $confidence = 'low'; // conflict → plurality, flagged return [$pattern, $confidence]; } /* Human label: "Daily", or "Mon, Wed, Fri", or "—" */ function rd_label(?string $pattern): string { if ($pattern === null || !preg_match('/^[01]{7}$/', $pattern)) return '—'; if ($pattern === '1111111') return 'Daily'; $names = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; $out = []; for ($i=0;$i<7;$i++) if ($pattern[$i]==='1') $out[] = $names[$i]; return $out ? implode(', ', $out) : '—'; } /* Does this train run on a given date? (Y-m-d) — uses cached pattern. */ function rd_runs_on(?string $pattern, string $ymd): ?bool { if ($pattern === null || !preg_match('/^[01]{7}$/', $pattern)) return null; // unknown $dow = (int)date('N', strtotime($ymd)); // 1=Mon..7=Sun return $pattern[$dow-1] === '1'; } /* ---------- Source adapters ---------- * Each returns [Mon..Sun] 0/1 array, or null on failure/unknown. * Keep each one small and isolated so one failing source never breaks others. */ // (1) irctc1 on RapidAPI — real adapter, only active if RAPIDAPI_KEY is set. function src_irctc1(string $trainNo): ?array { if (!defined('RAPIDAPI_KEY') || RAPIDAPI_KEY === '') return null; $url = 'https://irctc1.p.rapidapi.com/api/v1/getTrainSchedule?trainNo=' . urlencode($trainNo); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 12, CURLOPT_HTTPHEADER => [ 'x-rapidapi-host: irctc1.p.rapidapi.com', 'x-rapidapi-key: ' . RAPIDAPI_KEY, ], ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; $j = json_decode($body, true); // The field name varies by version; check the common ones, then fall back // to normalising whatever string we find. Verify with one real call first. $raw = $j['data']['train_base']['running_days'] ?? $j['data']['running_days'] ?? $j['train_base']['running_days'] ?? null; return rd_normalize(is_string($raw) ? $raw : null); } // (2) Template for an additional web/API source. Returns null until you wire it. // NOTE: if you point this at a public site, confirm its terms allow automated // access and expect to maintain the parser when their HTML changes. function src_secondary(string $trainNo): ?array { return null; // TODO: fetch + parse, then `return rd_normalize($daysTextFromPage);` } // (3) Another source slot — same contract. function src_tertiary(string $trainNo): ?array { return null; // TODO } function rd_adapters(): array { return [ 'irctc1' => 'src_irctc1', 'secondary' => 'src_secondary', 'tertiary' => 'src_tertiary', ]; } /* ---------- The resolver: consensus + confidence + cache ---------- */ function resolve_running_days(string $trainNo, int $maxAgeDays = 180): array { // 1) cache hit? $st = db()->prepare('SELECT * FROM train_running_days WHERE train_no = ? LIMIT 1'); $st->execute([$trainNo]); $row = $st->fetch(); if ($row && strtotime($row['checked_at']) > time() - $maxAgeDays*86400) { return [ 'pattern' => $row['pattern'], 'confidence' => $row['confidence'], 'label' => rd_label($row['pattern']), 'sources' => json_decode($row['sources_json'] ?? '[]', true) ?: [], 'cached' => true, ]; } // 2) query each source $perSource = []; // name => "1111111" | null foreach (rd_adapters() as $name => $fn) { $arr = null; try { $arr = $fn($trainNo); } catch (Throwable $e) { $arr = null; } $perSource[$name] = rd_to_string($arr); } // 3) vote [$pattern, $confidence] = rd_vote($perSource); // 4) cache + return $sources_json = json_encode($perSource); db()->prepare( 'INSERT INTO train_running_days (train_no, pattern, confidence, sources_json) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE pattern=VALUES(pattern), confidence=VALUES(confidence), sources_json=VALUES(sources_json), checked_at=NOW()' )->execute([$trainNo, $pattern, $confidence, $sources_json]); return [ 'pattern' => $pattern, 'confidence' => $confidence, 'label' => rd_label($pattern), 'sources' => $perSource, 'cached' => false, ]; } -------------------- END OF FILE -------------------- FILE: app/secrets.php TYPE: PHP SIZE: 2.09 KB ------------------------------------------------------------ Credentials) ---- // Authorised redirect URI to register there: // https://railsandroutes.com/oauth-callback.php define('GOOGLE_CLIENT_ID', '732275017842-gq3h2kc8gcpsu77v3573vmu013gou8gm.apps.googleusercontent.com'); define('GOOGLE_CLIENT_SECRET', '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'); // ---- First/bootstrap admin ---- // The first time THIS Google email signs in, it becomes an admin // automatically. After that, promote others from Admin > Users. define('BOOTSTRAP_ADMIN_EMAIL', 'kmsreekavi@gmail.com'); // ---- App secret (used to sign cookies/state). Make it long & random. ---- // Generate one, e.g. at https://www.random.org/strings/ or `openssl rand -hex 32` define('APP_KEY', 'J9#vQ7m!X2@rL8$kP4^nT1&wC6*zH3Yf'); // ---- OpenAI API key ---- define('OPENAI_API_KEY', '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'); // ---- RAPIDAPI_IRCTC1_KEY ---- define('RAPIDAPI_KEY', '4609cb7765msh244cbc83bc6600cp1748c6jsna528561829ec'); -------------------- END OF FILE -------------------- FILE: assets/css/style.css TYPE: CSS SIZE: 6.58 KB ------------------------------------------------------------ /* ============================================================ TrainChain — single-colour theme. All colour comes from --brand* variables injected per-request from the admin Settings page. Change the brand colour there and the entire UI re-tints. Mobile-first; safe-area aware for app wrappers. ============================================================ */ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} :root{ --ink:#16211f; --muted:#5b6b67; --line:#e4eae8; --bg:#f6f9f8; --card:#ffffff; --radius:16px; --shadow:0 1px 2px rgba(16,35,31,.06),0 8px 24px rgba(16,35,31,.06); --maxw:960px; } html{-webkit-text-size-adjust:100%} body{ font-family:"Outfit",system-ui,-apple-system,Segoe UI,Roboto,sans-serif; color:var(--ink);background:var(--bg);line-height:1.55; min-height:100dvh;display:flex;flex-direction:column; padding-top:env(safe-area-inset-top); } .wrap{width:100%;max-width:var(--maxw);margin-inline:auto;padding-inline:18px} a{color:var(--brand-700);text-decoration:none} h1,h2,h3{font-family:"Fraunces",Georgia,serif;font-weight:600;line-height:1.15;letter-spacing:-.01em} h1{font-size:clamp(1.7rem,4.5vw,2.5rem)} h2{font-size:clamp(1.3rem,3vw,1.7rem)} /* ---------- Top bar ---------- */ .topbar{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.86); backdrop-filter:saturate(160%) blur(10px);border-bottom:1px solid var(--line)} .topbar-in{display:flex;align-items:center;justify-content:space-between;min-height:60px} .brand{display:flex;align-items:center;gap:10px;color:var(--brand-700);font-weight:600} .brand-mark{display:grid;place-items:center;width:36px;height:36px;border-radius:11px; background:linear-gradient(150deg,var(--brand),var(--brand-700));color:var(--on-brand)} .brand-name{font-family:"Fraunces",serif;font-size:1.22rem;color:var(--ink);letter-spacing:-.01em} .nav{display:flex;align-items:center;gap:6px} .nav a{padding:9px 12px;border-radius:10px;color:var(--ink);font-weight:500;font-size:.96rem} .nav a:hover{background:var(--brand-50)} .nav-user{display:flex;align-items:center;gap:8px} .nav-user img{border-radius:50%} .nav-toggle{display:none;flex-direction:column;gap:5px;background:none;border:0;padding:10px;cursor:pointer} .nav-toggle span{width:22px;height:2px;background:var(--ink);border-radius:2px;transition:.2s} /* ---------- Layout ---------- */ .page{flex:1;padding-block:26px 40px;width:100%} .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius); box-shadow:var(--shadow);padding:22px} .card + .card{margin-top:16px} .grid{display:grid;gap:14px} .muted{color:var(--muted)} .center{text-align:center} /* ---------- Hero / search ---------- */ .hero{background:linear-gradient(160deg,var(--brand-50),var(--card));border:1px solid var(--line); border-radius:22px;padding:clamp(22px,5vw,40px);box-shadow:var(--shadow)} .hero p.tagline{color:var(--muted);font-size:1.05rem;margin-top:8px;max-width:46ch} .search-form{display:grid;gap:12px;margin-top:22px;grid-template-columns:1fr 1fr auto} .field{display:flex;flex-direction:column;gap:6px} .field label{font-size:.8rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em} input,select{font:inherit;color:var(--ink);background:#fff;border:1.5px solid var(--line); border-radius:12px;padding:13px 14px;width:100%;transition:border-color .15s,box-shadow .15s} input:focus,select:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 4px var(--brand-100)} /* ---------- Buttons ---------- */ .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;cursor:pointer; background:var(--brand);color:var(--on-brand);border:0;border-radius:12px; padding:13px 20px;font:inherit;font-weight:600;transition:transform .06s,filter .15s} .btn:hover{filter:brightness(.95)} .btn:active{transform:translateY(1px)} .btn-ghost{background:var(--brand-50);color:var(--brand-700)} .btn-sm{padding:9px 15px;font-size:.92rem} .btn-block{width:100%} .btn-google{background:#fff;color:#3c4043;border:1.5px solid var(--line);font-weight:600} .btn-google svg{width:19px;height:19px} /* ---------- Tables / results ---------- */ .result{border:1px solid var(--line);border-radius:14px;padding:16px;background:#fff;display:grid; grid-template-columns:1fr auto;gap:10px 16px;align-items:center} .result + .result{margin-top:12px} .result .tno{font-weight:600;color:var(--brand-700)} .result .times{display:flex;align-items:center;gap:12px;font-variant-numeric:tabular-nums} .result .times b{font-size:1.15rem} .badge{display:inline-block;background:var(--brand-50);color:var(--brand-700); border-radius:999px;padding:3px 10px;font-size:.78rem;font-weight:600} table.tbl{width:100%;border-collapse:collapse} table.tbl th,table.tbl td{text-align:left;padding:11px 10px;border-bottom:1px solid var(--line);font-size:.94rem} table.tbl th{color:var(--muted);font-weight:600;font-size:.8rem;text-transform:uppercase;letter-spacing:.03em} /* ---------- Flash ---------- */ .flash{border-radius:12px;padding:12px 16px;margin-bottom:16px;font-weight:500;border:1px solid} .flash-info{background:var(--brand-50);border-color:var(--brand-100);color:var(--brand-700)} .flash-success{background:#e9f7ef;border-color:#bfe6cf;color:#1c6b3f} .flash-error{background:#fdecec;border-color:#f6c9c9;color:#a32626} /* ---------- Footer ---------- */ .footer{border-top:1px solid var(--line);background:#fff; padding-bottom:calc(18px + env(safe-area-inset-bottom))} .footer-in{padding-block:22px;color:var(--muted);font-size:.9rem} .footer-note{margin-top:4px;font-size:.82rem} /* ---------- Forms layout helper ---------- */ .form-row{display:grid;gap:6px;margin-bottom:16px} .color-pick{display:flex;align-items:center;gap:12px} .color-pick input[type=color]{width:54px;height:46px;padding:4px;cursor:pointer} .swatches{display:flex;gap:8px;flex-wrap:wrap} .swatch{width:34px;height:34px;border-radius:9px;border:2px solid #fff;box-shadow:0 0 0 1px var(--line);cursor:pointer} /* ---------- Mobile ---------- */ @media (max-width:720px){ .nav-toggle{display:flex} .nav{position:fixed;inset:60px 0 auto 0;flex-direction:column;align-items:stretch;gap:2px; background:#fff;border-bottom:1px solid var(--line);padding:10px 18px 16px; transform:translateY(-130%);transition:transform .22s ease;box-shadow:var(--shadow)} body.nav-open .nav{transform:translateY(0)} .nav a{padding:13px 12px} .search-form{grid-template-columns:1fr} .result{grid-template-columns:1fr} } /* multi-field search form (from/to/date/time/go) */ .search-form-2{grid-template-columns:1.3fr 1.3fr .9fr .8fr auto} @media (max-width:720px){ .search-form-2{grid-template-columns:1fr} } .field-go{justify-content:flex-end} -------------------- END OF FILE -------------------- FILE: assets/img/favicon.svg TYPE: SVG SIZE: 471 B ------------------------------------------------------------ [IMAGE FILE: SVG - Content not displayed] -------------------- END OF FILE -------------------- FILE: assets/js/ai_vs_irctc.js TYPE: JS SIZE: 7.11 KB ------------------------------------------------------------ /* ai_vs_irctc.js — irctc1 truth vs OpenAI, with origin->boarding day-offset correction. irctc1 reports running days AT the from-station. OpenAI reports ORIGIN days. We shift OpenAI's answer by the train's stored day_offset at the from-station, then compare. Trains not in our timetable (offset unknown) are shown but not scored. */ (function () { var ORDER = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; var IDX = { Mon: 0, Tue: 1, Wed: 2, Thu: 3, Fri: 4, Sat: 5, Sun: 6 }; function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function shiftDays(days, off) { off = ((off % 7) + 7) % 7; var set = {}; days.forEach(function (d) { if (d in IDX) set[ORDER[(IDX[d] + off) % 7]] = 1; }); return ORDER.filter(function (d) { return set[d]; }); } document.addEventListener('DOMContentLoaded', function () { var form = document.getElementById('cmp-form'); if (!form) return; form.addEventListener('submit', function (ev) { ev.preventDefault(); var btn = document.getElementById('cmp-run'); var orig = btn.textContent; var csrf = form.querySelector('input[name=csrf]').value; var model = (document.getElementById('model').value || '').trim() || 'gpt-5.5'; var max = parseInt(document.getElementById('max').value, 10) || 6; var tbody = document.getElementById('cmp-rows'); var summary = document.getElementById('cmp-summary'); var box = document.getElementById('cmp-results'); tbody.innerHTML = ''; summary.innerHTML = ''; box.style.display = 'block'; btn.disabled = true; btn.textContent = 'Fetching irctc1 truth…'; var p = new URLSearchParams({ action: 'irctc', csrf: csrf, from: document.getElementById('from').value, to: document.getElementById('to').value, date: document.getElementById('date').value }); fetch(window.location.pathname, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: p }) .then(function (r) { if (!r.ok) throw new Error('irctc HTTP ' + r.status); return r.json(); }) .then(function (data) { if (data.err) throw new Error(data.err); var trains = (data.trains || []).slice(0, max); if (!trains.length) { summary.innerHTML = errBox('irctc1 returned no trains for this route/date.'); reset(); return; } var rows = []; trains.forEach(function (t) { var tr = document.createElement('tr'); tr.style.borderBottom = '1px solid #eee'; tr.style.verticalAlign = 'top'; tr.innerHTML = '' + esc(t.tn) + '' + '' + esc(t.name) + '' + '' + esc((t.days || []).join(',') || '(none)') + '' + '…querying OpenAI…'; tbody.appendChild(tr); rows.push({ t: t, tr: tr }); }); var i = 0, pass = 0, fail = 0, errc = 0, judged = 0; function next() { if (i >= rows.length) { var cls = (fail > 0 || errc > 0) ? 'flash-error' : 'flash-info'; var m = 'PASS ' + pass + ' \u00b7 FAIL ' + fail + ' \u00b7 ERROR ' + errc + '. '; if (judged === 0) m += 'Nothing scored (no truth or no offset data).'; else if (fail > 0) m += 'After offset correction, OpenAI still disagreed with irctc1 on ' + fail + ' of ' + judged + ' scored train(s) — those are real differences. Per your rule, irctc1 wins.'; else if (errc > 0) m += 'OpenAI matched on all ' + judged + ' scored, but some calls errored/timed out — rerun those (try gpt-5.4-mini) before concluding.'; else m += 'OpenAI matched irctc1 on all ' + judged + ' scored (offset-corrected). Widen across routes and include obscure/weekly trains before trusting it.'; summary.innerHTML = '
' + m + '
'; reset(); return; } var row = rows[i++]; var t = row.t; var tr = row.tr; var truth = (t.days || []).join(','); btn.textContent = 'OpenAI: ' + t.tn + ' (' + i + '/' + rows.length + ')…'; var b = new URLSearchParams({ action: 'one', csrf: csrf, train: t.tn, model: model }); fetch(window.location.pathname, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: b }) .then(function (r) { if (!r.ok) { var x = r.status === 504 ? ' (gateway timeout — try gpt-5.4-mini)' : ''; throw new Error('HTTP ' + r.status + x); } return r.json(); }) .then(function (d) { if (d.err) { errc++; fillErr(tr, d.err); return; } var originDays = d.got || []; var off = t.offset; var oCell, match; if (off === null || off === undefined) { oCell = esc(originDays.join(',') || '(none)') + ' (origin; no offset data — not scored)'; match = null; } else { var shifted = off === 0 ? originDays : shiftDays(originDays, off); oCell = (off === 0) ? esc(originDays.join(',') || '(none)') : esc(originDays.join(',') || '(none)') + ' \u2192 ' + esc(shifted.join(',') || '(none)') + ' (off +' + off + ')'; match = truth === '' ? null : (shifted.join(',') === truth); } if (match === true) pass++; else if (match === false) fail++; if (match !== null) judged++; fillResult(tr, oCell, match, d.conf, d.src); }) .catch(function (e) { errc++; fillErr(tr, e.message); }) .then(next); } next(); }) .catch(function (e) { summary.innerHTML = errBox(e.message); reset(); }); function reset() { btn.disabled = false; btn.textContent = orig; } function errBox(msg) { return '
' + esc(msg) + '
'; } function fillResult(tr, openaiHtml, match, conf, src) { var color = match === true ? '#197b3a' : (match === false ? '#c0392b' : '#888'); var label = match === true ? 'PASS' : (match === false ? 'FAIL' : '—'); while (tr.children.length > 3) tr.removeChild(tr.children[3]); tr.insertAdjacentHTML('beforeend', '' + openaiHtml + '' + '' + label + '' + '' + esc(conf) + '' + '' + esc(src) + ''); } function fillErr(tr, msg) { while (tr.children.length > 3) tr.removeChild(tr.children[3]); tr.insertAdjacentHTML('beforeend', '\u26a0 ' + esc(msg) + ''); } }); }); })(); -------------------- END OF FILE -------------------- FILE: assets/js/app.js TYPE: JS SIZE: 3.04 KB ------------------------------------------------------------ /* TrainChain — lightweight front-end (no dependencies). */ (function () { "use strict"; // Close mobile nav when a link is tapped or on resize. document.querySelectorAll(".nav a").forEach(function (a) { a.addEventListener("click", function () { document.body.classList.remove("nav-open"); }); }); window.addEventListener("resize", function () { if (window.innerWidth > 720) document.body.classList.remove("nav-open"); }); // Station autocomplete for any . document.querySelectorAll("input[data-station]").forEach(function (input) { var box = document.createElement("div"); box.className = "ac-box"; box.style.cssText = "position:absolute;z-index:60;background:#fff;border:1px solid var(--line);" + "border-radius:12px;box-shadow:var(--shadow);margin-top:4px;max-height:240px;" + "overflow:auto;display:none;min-width:220px;max-width:calc(100vw - 36px)"; input.parentNode.style.position = "relative"; input.parentNode.appendChild(box); var timer = null; input.addEventListener("input", function () { var q = input.value.trim(); clearTimeout(timer); if (q.length < 2) { box.style.display = "none"; return; } timer = setTimeout(function () { fetch("/stations.php?q=" + encodeURIComponent(q)) .then(function (r) { return r.json(); }) .then(function (rows) { box.innerHTML = ""; if (!rows.length) { box.style.display = "none"; return; } rows.forEach(function (s) { var item = document.createElement("button"); item.type = "button"; item.textContent = s.name + " (" + s.code + ")"; item.style.cssText = "display:block;width:100%;text-align:left;border:0;background:none;" + "padding:11px 14px;cursor:pointer;font:inherit;color:var(--ink)"; item.addEventListener("mouseenter", function () { item.style.background = "var(--brand-50)"; }); item.addEventListener("mouseleave", function () { item.style.background = "none"; }); item.addEventListener("click", function () { input.value = s.name + " (" + s.code + ")"; input.dataset.code = s.code; box.style.display = "none"; }); box.appendChild(item); }); box.style.display = "block"; }) .catch(function () { box.style.display = "none"; }); }, 180); }); document.addEventListener("click", function (ev) { if (ev.target !== input) box.style.display = "none"; }); }); // Swap the From/To station fields (value + autocomplete code). window.rrSwap = function (btn) { var form = btn.closest("form"); if (!form) return; var a = form.querySelector('input[name="from"]'), b = form.querySelector('input[name="to"]'); if (!a || !b) return; var v = a.value; a.value = b.value; b.value = v; var c = a.dataset.code || ""; a.dataset.code = b.dataset.code || ""; b.dataset.code = c; }; })(); -------------------- END OF FILE -------------------- FILE: assets/js/rundays_test.js TYPE: JS SIZE: 4.85 KB ------------------------------------------------------------ /* rundays_test.js — runs the AI running-days test one train at a time so no single request hits the nginx gateway timeout. */ (function () { function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function rowOk(tn, exp, got, match, conf, src) { var color = match === true ? '#197b3a' : (match === false ? '#c0392b' : '#888'); var label = match === true ? 'PASS' : (match === false ? 'FAIL' : '—'); return '' + esc(tn) + '' + '' + esc(exp || '—') + '' + '' + esc(got || '(none)') + '' + '' + label + '' + '' + esc(conf) + '' + '' + esc(src) + ''; } function rowErr(tn, exp, msg) { return '' + esc(tn) + '' + '' + esc(exp || '—') + '' + '⚠ ' + esc(msg) + ''; } function verdict(p, f, e, judged) { var cls = (f > 0 || e > 0) ? 'flash-error' : 'flash-info'; var msg = 'PASS ' + p + ' · FAIL ' + f + ' · ERROR ' + e + '. '; if (judged === 0) msg += 'No answer key given, so nothing was judged — add expected days to decide.'; else if (f > 0) msg += 'Failed. Per your rule, drop the OpenAI approach and use irctc1.'; else if (e > 0) msg += 'Some calls errored/timed out (API, quota, or model too slow) — fix those before judging.'; else msg += 'Passed this set. A small pass is weak evidence — widen the list before trusting it.'; return '
' + msg + '
'; } document.addEventListener('DOMContentLoaded', function () { var form = document.getElementById('rd-form'); if (!form) return; form.addEventListener('submit', function (ev) { ev.preventDefault(); var btn = document.getElementById('rd-run'); var orig = btn.textContent; var model = (document.getElementById('model').value || '').trim() || 'gpt-5.5'; var csrf = form.querySelector('input[name=csrf]').value; var tbody = document.getElementById('rd-rows'); var summary = document.getElementById('rd-summary'); var box = document.getElementById('rd-results'); var lines = (document.getElementById('trains').value || '').split(/\r?\n/); tbody.innerHTML = ''; summary.innerHTML = ''; box.style.display = 'block'; btn.disabled = true; var jobs = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line) continue; var c = line.indexOf(':'); var tn = (c === -1 ? line : line.slice(0, c)).replace(/\D/g, ''); if (!tn) continue; var exp = c === -1 ? '' : line.slice(c + 1).trim(); jobs.push({ tn: tn, exp: exp }); } var idx = 0, pass = 0, fail = 0, err = 0, judged = 0; function next() { if (idx >= jobs.length) { btn.disabled = false; btn.textContent = orig; summary.innerHTML = verdict(pass, fail, err, judged); return; } var job = jobs[idx++]; btn.textContent = 'Testing ' + job.tn + ' (' + idx + '/' + jobs.length + ')…'; var row = document.createElement('tr'); row.style.borderBottom = '1px solid #eee'; row.style.verticalAlign = 'top'; row.innerHTML = '' + esc(job.tn) + '' + '' + esc(job.exp || '—') + '…querying…'; tbody.appendChild(row); var body = new URLSearchParams({ action: 'one', train: job.tn, exp: job.exp, model: model, csrf: csrf }); fetch(window.location.pathname, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }).then(function (resp) { if (!resp.ok) { var extra = resp.status === 504 ? ' (gateway timeout — model too slow for this host; try gpt-5.4-mini)' : ''; throw new Error('HTTP ' + resp.status + extra); } return resp.json(); }).then(function (data) { if (data.err) { err++; row.innerHTML = rowErr(job.tn, job.exp, data.err); } else { var got = (data.got || []).join(','); if (data.match === true) pass++; else if (data.match === false) fail++; if (data.match !== null) judged++; row.innerHTML = rowOk(job.tn, job.exp, got, data.match, data.conf, data.src); } }).catch(function (e) { err++; row.innerHTML = rowErr(job.tn, job.exp, e.message); }).then(next); } next(); }); }); })(); -------------------- END OF FILE --------------------