From d08b89256226c8869c5d9dfd777ebaf7c01965b9 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Thu, 18 Dec 2025 15:49:33 -0500 Subject: [PATCH 01/22] add prefix 'region/' to all continent labels for easier readability in github issues --- functions/packages/feed-form/src/impl/feed-form-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/packages/feed-form/src/impl/feed-form-impl.ts b/functions/packages/feed-form/src/impl/feed-form-impl.ts index f84b0f768..60f61ced2 100644 --- a/functions/packages/feed-form/src/impl/feed-form-impl.ts +++ b/functions/packages/feed-form/src/impl/feed-form-impl.ts @@ -327,7 +327,7 @@ async function createGithubIssue( if (formData.country && formData.country in countries) { const country = countries[formData.country as TCountryCode]; const continent = continents[country.continent].toLowerCase(); - if (continent != null) labels.push(continent); + if (continent != null) labels.push(`region/${continent}`); } try { From 20a8ca661b46bbeb50490ca4258f478646248364 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Thu, 18 Dec 2025 15:51:41 -0500 Subject: [PATCH 02/22] add auth required label when it is present in the feed form --- functions/packages/feed-form/src/impl/feed-form-impl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/functions/packages/feed-form/src/impl/feed-form-impl.ts b/functions/packages/feed-form/src/impl/feed-form-impl.ts index 60f61ced2..dadfce901 100644 --- a/functions/packages/feed-form/src/impl/feed-form-impl.ts +++ b/functions/packages/feed-form/src/impl/feed-form-impl.ts @@ -330,6 +330,10 @@ async function createGithubIssue( if (continent != null) labels.push(`region/${continent}`); } + if (formData.authType !== "None - 0") { + labels.push("auth required"); + } + try { const response = await axios.post( githubRepoUrlIssue, From 9a2fb11369a4562de70401afbef2cc4a3c30856b Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Thu, 18 Dec 2025 16:28:24 -0500 Subject: [PATCH 03/22] add URL check to see if it produces valid zip file --- .../feed-form/src/impl/feed-form-impl.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/functions/packages/feed-form/src/impl/feed-form-impl.ts b/functions/packages/feed-form/src/impl/feed-form-impl.ts index dadfce901..35e441dd4 100644 --- a/functions/packages/feed-form/src/impl/feed-form-impl.ts +++ b/functions/packages/feed-form/src/impl/feed-form-impl.ts @@ -334,6 +334,12 @@ async function createGithubIssue( labels.push("auth required"); } + if (!isValidZipUrl(formData.feedLink)) { + if(!await isValidZipDownload(formData.feedLink)) { + labels.push("invalid"); + } + } + try { const response = await axios.post( githubRepoUrlIssue, @@ -445,3 +451,38 @@ export function buildGithubIssueBody( return content; } /* eslint-enable */ + +/** + * Parses the provided URL to check if it is a valid ZIP file URL + * @param url The direct download URL provided in the feed form + * @returns {boolean} Whether the URL is a valid ZIP file URL + */ +function isValidZipUrl(url: string | undefined | null): boolean { + if (!url) return false; + try { + const parsed = new URL(url); + return parsed.pathname.toLowerCase().endsWith(".zip"); + } catch { + return false; + } +} + +/** + * Checks if the provided URL points to a valid ZIP file by making a HEAD request + * @param url The direct download URL provided in the feed form + * @returns {boolean} Whether the URL downloads a valid ZIP file + */ +async function isValidZipDownload(url: string | undefined | null): Promise { + try { + if (!url) return false; + const response = await axios.head(url, {maxRedirects: 2}); + const contentType = response.headers["content-type"]; + const contentDisposition = response.headers["content-disposition"]; + + if (contentType && contentType.includes("zip")) return true; + if (contentDisposition && contentDisposition.includes("zip")) return true; + return false; + } catch { + return false; + } +} From 09ae736b2ff1c5df3851e6d12298e02e4b439e07 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Thu, 18 Dec 2025 20:23:05 -0500 Subject: [PATCH 04/22] add checks in feed form for pre-existing feeds based on direct download URL --- functions/packages/feed-form/package.json | 4 +- web-app/public/locales/en/feeds.json | 1 + .../screens/FeedSubmission/Form/FirstStep.tsx | 26 ++++- .../Form/SecondStepRealtime.tsx | 103 +++++++++++++----- web-app/src/app/services/feeds/utils.ts | 26 +++++ 5 files changed, 125 insertions(+), 35 deletions(-) diff --git a/functions/packages/feed-form/package.json b/functions/packages/feed-form/package.json index 74758badc..17914a759 100644 --- a/functions/packages/feed-form/package.json +++ b/functions/packages/feed-form/package.json @@ -22,10 +22,12 @@ "firebase": "^10.6.0", "firebase-admin": "^11.8.0", "firebase-functions": "^4.3.1", - "google-spreadsheet": "^4.1.2" + "google-spreadsheet": "^4.1.2", + "papaparse": "^5.5.3" }, "devDependencies": { "@types/jest": "^29.5.8", + "@types/papaparse": "^5.5.2", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "^5.12.0", "eslint": "^8.9.0", diff --git a/web-app/public/locales/en/feeds.json b/web-app/public/locales/en/feeds.json index 82a8765f1..583d126a6 100644 --- a/web-app/public/locales/en/feeds.json +++ b/web-app/public/locales/en/feeds.json @@ -46,6 +46,7 @@ "errorSubmitting": "An error occurred while submitting the form.", "submittingFeed": "Submitting the feed...", "errorUrl": "The URL must start with a valid protocol: http:// or https://", + "feedAlreadyExists": "This feed URL already exists in the Mobility Database: ", "unofficialDesc": "Why was this feed created?", "unofficialDescPlaceholder": "Does this feed exist for research purposes, a specific app, etc?", "updateFreq": "How often is this feed updated?", diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index 29fe8f0e8..2482ea6db 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -21,7 +21,7 @@ import { import { type YesNoFormInput, type FeedSubmissionFormFormInput } from '.'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { isValidFeedLink } from '../../../services/feeds/utils'; +import { isValidFeedLink, checkFeedUrlExistsInCsv } from '../../../services/feeds/utils'; import FormLabelDescription from './components/FormLabelDescription'; export interface FeedSubmissionFormFormInputFirstStep { @@ -267,8 +267,14 @@ export default function FormFirstStep({ - isValidFeedLink(value ?? '') || t('form.errorUrl'), + validate: async (value) => { + if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + const exists = await checkFeedUrlExistsInCsv(value ?? ''); + if (exists) { + return `Feed Exists:${exists}`; + } + return true; + }, }} control={control} name='feedLink' @@ -276,9 +282,21 @@ export default function FormFirstStep({ + {t('form.feedAlreadyExists')} + + {t(errors.feedLink.message.replace('Feed Exists:',''))} + + + ) : ( + errors.feedLink?.message ?? '' + ) + } /> )} /> diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index b69be0742..fbcdad3ac 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -10,7 +10,7 @@ import { type SubmitHandler, Controller, useForm } from 'react-hook-form'; import { type AuthTypes, type FeedSubmissionFormFormInput } from '.'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { isValidFeedLink } from '../../../services/feeds/utils'; +import { isValidFeedLink, checkFeedUrlExistsInCsv } from '../../../services/feeds/utils'; export interface FeedSubmissionFormInputSecondStepRT { tripUpdates: string; @@ -78,29 +78,6 @@ export default function FormSecondStepRT({ } }, [tripUpdates, vehiclePositions, serviceAlerts]); - const gtfsRtLinkValidation = ( - rtType: 'tu' | 'vp' | 'sa', - ): boolean | string => { - if (tripUpdates !== '' || vehiclePositions !== '' || serviceAlerts !== '') { - switch (rtType) { - case 'tu': - return tripUpdates !== '' - ? isValidFeedLink(tripUpdates) || t('form.errorUrl') - : true; - case 'vp': - return vehiclePositions !== '' - ? isValidFeedLink(vehiclePositions) || t('form.errorUrl') - : true; - case 'sa': - return serviceAlerts !== '' - ? isValidFeedLink(serviceAlerts) || t('form.errorUrl') - : true; - } - } else { - return t('form.atLeastOneRealtimeFeed'); - } - }; - return ( <> gtfsRtLinkValidation('sa') }} + rules={{ + required: t('form.feedLinkRequired'), + validate: async (value) => { + if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + const exists = await checkFeedUrlExistsInCsv(value ?? ''); + if (exists) { + return `Feed Exists:${exists}`; + } + return true; + }, + }} render={({ field }) => ( + {t('form.feedAlreadyExists')} + + {t(errors.serviceAlerts.message.replace('Feed Exists:',''))} + + + ) : ( + errors.serviceAlerts?.message ?? '' + ) + } /> )} /> @@ -181,13 +180,35 @@ export default function FormSecondStepRT({ gtfsRtLinkValidation('tu') }} + rules={{ + required: t('form.feedLinkRequired'), + validate: async (value) => { + if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + const exists = await checkFeedUrlExistsInCsv(value ?? ''); + if (exists) { + return `Feed Exists:${exists}`; + } + return true; + }, + }} render={({ field }) => ( + {t('form.feedAlreadyExists')} + + {t(errors.tripUpdates.message.replace('Feed Exists:',''))} + + + ) : ( + errors.tripUpdates?.message ?? '' + ) + } /> )} /> @@ -236,13 +257,35 @@ export default function FormSecondStepRT({ gtfsRtLinkValidation('vp') }} + rules={{ + required: t('form.feedLinkRequired'), + validate: async (value) => { + if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + const exists = await checkFeedUrlExistsInCsv(value ?? ''); + if (exists) { + return `Feed Exists:${exists}`; + } + return true; + }, + }} render={({ field }) => ( + {t('form.feedAlreadyExists')} + + {t(errors.vehiclePositions.message.replace('Feed Exists:',''))} + + + ) : ( + errors.vehiclePositions?.message ?? '' + ) + } /> )} /> diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index db39ad526..3fb15335e 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -1,3 +1,4 @@ +import Papa from 'papaparse'; import { getEmojiFlag, type TCountryCode, languages } from 'countries-list'; import { type paths, type components } from './types'; @@ -150,3 +151,28 @@ export const langCodeToName = (code: string): string => { const lang = languages[primary as keyof typeof languages]; return lang?.name ?? code.toUpperCase(); }; + +/** + * Checks if a feed URL exists in the urls.direct_download column of the feeds_v2.csv file. + * @param feedUrl The URL to check for existence. + * @param csvUrl The CSV file URL (default: feeds_v2.csv from Mobility Database) + * @returns Promise true if exists, false otherwise + */ +export async function checkFeedUrlExistsInCsv( + feedUrl: string, + csvUrl = 'https://files.mobilitydatabase.org/feeds_v2.csv' +): Promise { + try { + const response = await fetch(csvUrl); + if (!response.ok) throw new Error('Failed to fetch CSV'); + const csvText = await response.text(); + const parsed = Papa.parse(csvText, { header: true }); + if (!parsed.data || !Array.isArray(parsed.data)) return null; + const match = (parsed.data as any[]).find( + (row) => row['urls.direct_download'] === feedUrl + ); + return match ? match['id'] : null; + } catch (e) { + return null + } +} From 1a4e19fc82d04001367d570824210e6a6c8c3f2c Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Thu, 18 Dec 2025 20:58:35 -0500 Subject: [PATCH 05/22] make the mbd URL prefix a constant --- web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx | 4 +++- .../screens/FeedSubmission/Form/SecondStepRealtime.tsx | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index 2482ea6db..cc2da0550 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -42,6 +42,8 @@ interface FormFirstStepProps { setNumberOfSteps: (numberOfSteps: YesNoFormInput) => void; } +const realtimeFeedURLPrefix = "https://mobilitydatabase.org/feeds/gtfs/"; + export default function FormFirstStep({ initialValues, submitFormData, @@ -289,7 +291,7 @@ export default function FormFirstStep({ {t('form.feedAlreadyExists')} + {errors.feedLink.message.replace('Feed Exists:', `${realtimeFeedURLPrefix}`)} target="_blank" rel="noopener noreferrer"> {t(errors.feedLink.message.replace('Feed Exists:',''))} diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index fbcdad3ac..a87fd0605 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -32,6 +32,8 @@ interface FormSecondStepRTProps { handleBack: (formData: Partial) => void; } +const realtimeFeedURLPrefix = "https://mobilitydatabase.org/feeds/gtfs_rt/"; + export default function FormSecondStepRT({ initialValues, submitFormData, @@ -124,7 +126,7 @@ export default function FormSecondStepRT({ {t('form.feedAlreadyExists')} + {errors.serviceAlerts.message.replace('Feed Exists:', `${realtimeFeedURLPrefix}`)} target="_blank" rel="noopener noreferrer"> {t(errors.serviceAlerts.message.replace('Feed Exists:',''))} @@ -201,7 +203,7 @@ export default function FormSecondStepRT({ {t('form.feedAlreadyExists')} + {errors.tripUpdates.message.replace('Feed Exists:', `${realtimeFeedURLPrefix}`)} target="_blank" rel="noopener noreferrer"> {t(errors.tripUpdates.message.replace('Feed Exists:',''))} @@ -278,7 +280,7 @@ export default function FormSecondStepRT({ {t('form.feedAlreadyExists')} + {errors.vehiclePositions.message.replace('Feed Exists:', `${realtimeFeedURLPrefix}`)} target="_blank" rel="noopener noreferrer"> {t(errors.vehiclePositions.message.replace('Feed Exists:',''))} From ca54fddb9f641d7b689dc7f86fbc148c0bb69a80 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 11:02:07 -0500 Subject: [PATCH 06/22] fix linter errors --- .../screens/FeedSubmission/Form/FirstStep.tsx | 21 +++++++---- .../Form/SecondStepRealtime.tsx | 37 ++++++++++++------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index cc2da0550..724852042 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -21,7 +21,7 @@ import { import { type YesNoFormInput, type FeedSubmissionFormFormInput } from '.'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { isValidFeedLink, checkFeedUrlExistsInCsv } from '../../../services/feeds/utils'; +import { isValidFeedLink, checkFeedUrlExistsInCsv, } from '../../../services/feeds/utils'; import FormLabelDescription from './components/FormLabelDescription'; export interface FeedSubmissionFormFormInputFirstStep { @@ -42,7 +42,7 @@ interface FormFirstStepProps { setNumberOfSteps: (numberOfSteps: YesNoFormInput) => void; } -const realtimeFeedURLPrefix = "https://mobilitydatabase.org/feeds/gtfs/"; +const realtimeFeedURLPrefix = 'https://mobilitydatabase.org/feeds/gtfs/'; export default function FormFirstStep({ initialValues, @@ -270,9 +270,11 @@ export default function FormFirstStep({ rules={{ required: t('form.feedLinkRequired'), validate: async (value) => { - if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + if (!isValidFeedLink(value ?? '')) { + return t('form.errorUrl'); + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); - if (exists) { + if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; } return true; @@ -287,12 +289,15 @@ export default function FormFirstStep({ error={errors.feedLink !== undefined} {...field} helperText={ - errors.feedLink?.message?.startsWith('Feed Exists:') ? ( + typeof errors.feedLink?.message === 'string' && errors.feedLink.message.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} - - {t(errors.feedLink.message.replace('Feed Exists:',''))} + + {t(errors.feedLink.message.replace('Feed Exists:', ''))} ) : ( diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index a87fd0605..fd6f1982d 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -10,7 +10,7 @@ import { type SubmitHandler, Controller, useForm } from 'react-hook-form'; import { type AuthTypes, type FeedSubmissionFormFormInput } from '.'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { isValidFeedLink, checkFeedUrlExistsInCsv } from '../../../services/feeds/utils'; +import { isValidFeedLink, checkFeedUrlExistsInCsv, } from '../../../services/feeds/utils'; export interface FeedSubmissionFormInputSecondStepRT { tripUpdates: string; @@ -32,7 +32,7 @@ interface FormSecondStepRTProps { handleBack: (formData: Partial) => void; } -const realtimeFeedURLPrefix = "https://mobilitydatabase.org/feeds/gtfs_rt/"; +const realtimeFeedURLPrefix = 'https://mobilitydatabase.org/feeds/gtfs_rt/'; export default function FormSecondStepRT({ initialValues, @@ -125,10 +125,13 @@ export default function FormSecondStepRT({ errors.serviceAlerts?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} - - {t(errors.serviceAlerts.message.replace('Feed Exists:',''))} - + + {t(errors.serviceAlerts.message.replace('Feed Exists:', ''))} + ) : ( errors.serviceAlerts?.message ?? '' @@ -202,10 +205,13 @@ export default function FormSecondStepRT({ errors.tripUpdates?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} - - {t(errors.tripUpdates.message.replace('Feed Exists:',''))} - + + {t(errors.tripUpdates.message.replace('Feed Exists:', ''))} + ) : ( errors.tripUpdates?.message ?? '' @@ -279,10 +285,13 @@ export default function FormSecondStepRT({ errors.vehiclePositions?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} - - {t(errors.vehiclePositions.message.replace('Feed Exists:',''))} - + + {t(errors.vehiclePositions.message.replace('Feed Exists:', ''))} + ) : ( errors.vehiclePositions?.message ?? '' From 72ec2811220ceee62edc19ddd3a41223a00ff99f Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 11:15:13 -0500 Subject: [PATCH 07/22] fix linter errors --- .../screens/FeedSubmission/Form/FirstStep.tsx | 16 +++++--- .../Form/SecondStepRealtime.tsx | 41 +++++++++++-------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index 724852042..e4a25d8b1 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -21,7 +21,10 @@ import { import { type YesNoFormInput, type FeedSubmissionFormFormInput } from '.'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { isValidFeedLink, checkFeedUrlExistsInCsv, } from '../../../services/feeds/utils'; +import { + isValidFeedLink, + checkFeedUrlExistsInCsv, +} from '../../../services/feeds/utils'; import FormLabelDescription from './components/FormLabelDescription'; export interface FeedSubmissionFormFormInputFirstStep { @@ -42,7 +45,7 @@ interface FormFirstStepProps { setNumberOfSteps: (numberOfSteps: YesNoFormInput) => void; } -const realtimeFeedURLPrefix = 'https://mobilitydatabase.org/feeds/gtfs/'; +const scheduleFeedURLPrefix = 'https://mobilitydatabase.org/feeds/gtfs/'; export default function FormFirstStep({ initialValues, @@ -293,9 +296,12 @@ export default function FormFirstStep({ {t('form.feedAlreadyExists')} {t(errors.feedLink.message.replace('Feed Exists:', ''))} diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index fd6f1982d..cf367872d 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -10,7 +10,10 @@ import { type SubmitHandler, Controller, useForm } from 'react-hook-form'; import { type AuthTypes, type FeedSubmissionFormFormInput } from '.'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { isValidFeedLink, checkFeedUrlExistsInCsv, } from '../../../services/feeds/utils'; +import { + isValidFeedLink, + checkFeedUrlExistsInCsv, +} from '../../../services/feeds/utils'; export interface FeedSubmissionFormInputSecondStepRT { tripUpdates: string; @@ -107,9 +110,11 @@ export default function FormSecondStepRT({ rules={{ required: t('form.feedLinkRequired'), validate: async (value) => { - if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + if (!isValidFeedLink(value ?? '')) { + return t('form.errorUrl'); + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); - if (exists) { + if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; } return true; @@ -122,13 +127,13 @@ export default function FormSecondStepRT({ error={errors.serviceAlerts !== undefined} data-cy='serviceAlertFeed' helperText={ - errors.serviceAlerts?.message?.startsWith('Feed Exists:') ? ( + typeof errors.serviceAlerts?.message === 'string' && errors.serviceAlerts?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} {t(errors.serviceAlerts.message.replace('Feed Exists:', ''))} @@ -188,9 +193,11 @@ export default function FormSecondStepRT({ rules={{ required: t('form.feedLinkRequired'), validate: async (value) => { - if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + if (!isValidFeedLink(value ?? '')) { + return t('form.errorUrl'); + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); - if (exists) { + if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; } return true; @@ -202,13 +209,13 @@ export default function FormSecondStepRT({ {...field} error={errors.tripUpdates !== undefined} helperText={ - errors.tripUpdates?.message?.startsWith('Feed Exists:') ? ( + typeof errors.tripUpdates?.message === 'string' && errors.tripUpdates?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} {t(errors.tripUpdates.message.replace('Feed Exists:', ''))} @@ -268,9 +275,11 @@ export default function FormSecondStepRT({ rules={{ required: t('form.feedLinkRequired'), validate: async (value) => { - if (!isValidFeedLink(value ?? '')) return t('form.errorUrl'); + if (!isValidFeedLink(value ?? '')) { + return t('form.errorUrl'); + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); - if (exists) { + if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; } return true; @@ -282,13 +291,13 @@ export default function FormSecondStepRT({ {...field} error={errors.vehiclePositions !== undefined} helperText={ - errors.vehiclePositions?.message?.startsWith('Feed Exists:') ? ( + typeof errors.vehiclePositions?.message === 'string' && errors.vehiclePositions?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} {t(errors.vehiclePositions.message.replace('Feed Exists:', ''))} From c40ed8d989009a0863b428751d35a2ce542abc73 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 11:23:23 -0500 Subject: [PATCH 08/22] fix linter errors --- .../screens/FeedSubmission/Form/FirstStep.tsx | 2 +- .../Form/SecondStepRealtime.tsx | 51 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index e4a25d8b1..40ec41057 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -298,7 +298,7 @@ export default function FormFirstStep({ {t('form.feedAlreadyExists')} - - {t(errors.serviceAlerts.message.replace('Feed Exists:', ''))} - + + {t(errors.serviceAlerts.message.replace('Feed Exists:', ''))} + ) : ( errors.serviceAlerts?.message ?? '' @@ -212,13 +215,16 @@ export default function FormSecondStepRT({ typeof errors.tripUpdates?.message === 'string' && errors.tripUpdates?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} - - {t(errors.tripUpdates.message.replace('Feed Exists:', ''))} - + + {t(errors.tripUpdates.message.replace('Feed Exists:', ''))} + ) : ( errors.tripUpdates?.message ?? '' @@ -294,13 +300,16 @@ export default function FormSecondStepRT({ typeof errors.vehiclePositions?.message === 'string' && errors.vehiclePositions?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} - - {t(errors.vehiclePositions.message.replace('Feed Exists:', ''))} - + + {t(errors.vehiclePositions.message.replace('Feed Exists:', ''))} + ) : ( errors.vehiclePositions?.message ?? '' From c1bba5e6c9b4cc93e93f2389361c9acd78f06d59 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 11:34:18 -0500 Subject: [PATCH 09/22] fix linter errors --- .../Form/SecondStepRealtime.tsx | 26 ++++++++++++++++--- web-app/src/app/services/feeds/utils.ts | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index a7bd49ef7..ab2625a2d 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -138,7 +138,12 @@ export default function FormSecondStepRT({ target='_blank' rel='noopener noreferrer' > - {t(errors.serviceAlerts.message.replace('Feed Exists:', ''))} + {t( + errors.serviceAlerts.message.replace( + 'Feed Exists:', + '', + ), + )} ) : ( @@ -223,7 +228,12 @@ export default function FormSecondStepRT({ target='_blank' rel='noopener noreferrer' > - {t(errors.tripUpdates.message.replace('Feed Exists:', ''))} + {t( + errors.tripUpdates.message.replace( + 'Feed Exists:', + '', + ), + )} ) : ( @@ -297,7 +307,10 @@ export default function FormSecondStepRT({ {...field} error={errors.vehiclePositions !== undefined} helperText={ - typeof errors.vehiclePositions?.message === 'string' && errors.vehiclePositions?.message?.startsWith('Feed Exists:') ? ( + typeof + errors.vehiclePositions?.message === 'string' && errors.vehiclePositions?.message?.startsWith + ('Feed Exists:', + ) ? ( {t('form.feedAlreadyExists')} - {t(errors.vehiclePositions.message.replace('Feed Exists:', ''))} + {t( + errors.vehiclePositions.message.replace( + 'Feed Exists:', + '', + ), + )} ) : ( diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index 3fb15335e..3c88ecb89 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -160,7 +160,7 @@ export const langCodeToName = (code: string): string => { */ export async function checkFeedUrlExistsInCsv( feedUrl: string, - csvUrl = 'https://files.mobilitydatabase.org/feeds_v2.csv' + csvUrl = 'https://files.mobilitydatabase.org/feeds_v2.csv', ): Promise { try { const response = await fetch(csvUrl); From 74fd4ebae1e04639fb9a473e18400a08c832c0ee Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 11:49:20 -0500 Subject: [PATCH 10/22] fix linter errors --- .../FeedSubmission/Form/SecondStepRealtime.tsx | 12 ++++++------ web-app/src/app/services/feeds/utils.ts | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index ab2625a2d..1df118daa 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -127,7 +127,8 @@ export default function FormSecondStepRT({ error={errors.serviceAlerts !== undefined} data-cy='serviceAlertFeed' helperText={ - typeof errors.serviceAlerts?.message === 'string' && errors.serviceAlerts?.message?.startsWith('Feed Exists:') ? ( + errors.serviceAlerts?.message === 'string' && + errors.serviceAlerts?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} row['urls.direct_download'] === feedUrl - ); - return match ? match['id'] : null; + if (!parsed.data || !Array.isArray(parsed.data) || !parsed.data.every((row) => typeof row === 'object' && row !== null)) { + return null; + } + type FeedCsvRow = { 'urls.direct_download'?: string; id?: string }; + const rows = parsed.data as FeedCsvRow[]; + const match = rows.find((row) => row['urls.direct_download'] === feedUrl); + return match?.id ?? null; } catch (e) { return null } From 90e9db2ba2625350829f7d513f732042fd65b046 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 11:58:32 -0500 Subject: [PATCH 11/22] fix linter errors --- .../app/screens/FeedSubmission/Form/FirstStep.tsx | 14 +++++++++++--- .../FeedSubmission/Form/SecondStepRealtime.tsx | 12 +++++++++--- web-app/src/app/services/feeds/utils.ts | 14 ++++++-------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index 40ec41057..aaab8b3f2 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -292,8 +292,11 @@ export default function FormFirstStep({ error={errors.feedLink !== undefined} {...field} helperText={ - typeof errors.feedLink?.message === 'string' && errors.feedLink.message.startsWith('Feed Exists:') ? ( - + errors.feedLink?.message === 'string' && + errors.feedLink?.message?.startsWith( + 'Feed Exists:', + ) ? ( + {t('form.feedAlreadyExists')} - {t(errors.feedLink.message.replace('Feed Exists:', ''))} + {t( + errors.feedLink.message.replace( + 'Feed Exists:', + '', + ), + )} ) : ( diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index 1df118daa..5b6c9d37b 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -128,7 +128,9 @@ export default function FormSecondStepRT({ data-cy='serviceAlertFeed' helperText={ errors.serviceAlerts?.message === 'string' && - errors.serviceAlerts?.message?.startsWith('Feed Exists:') ? ( + errors.serviceAlerts?.message?.startsWith( + 'Feed Exists:', + ) ? ( {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} typeof row === 'object' && row !== null)) { - return null; - } - type FeedCsvRow = { 'urls.direct_download'?: string; id?: string }; - const rows = parsed.data as FeedCsvRow[]; - const match = rows.find((row) => row['urls.direct_download'] === feedUrl); - return match?.id ?? null; + if (!parsed.data || !Array.isArray(parsed.data)) return null; + const match = (parsed.data as any[]).find( + (row) => row['urls.direct_download'] === feedUrl, + ); + return match ? match['id'] : null; } catch (e) { - return null + return null; } } From 940f349f2aa32702228342035cb481e449f1bafd Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 12:10:16 -0500 Subject: [PATCH 12/22] fix linter errors --- .../screens/FeedSubmission/Form/FirstStep.tsx | 8 +++----- .../Form/SecondStepRealtime.tsx | 18 +++++++++--------- web-app/src/app/services/feeds/utils.ts | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index aaab8b3f2..80915e442 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -292,11 +292,9 @@ export default function FormFirstStep({ error={errors.feedLink !== undefined} {...field} helperText={ - errors.feedLink?.message === 'string' && - errors.feedLink?.message?.startsWith( - 'Feed Exists:', - ) ? ( - + errors.feedLink?.message === 'string' && + errors.feedLink?.message?.startsWith('Feed Exists:') ? ( + {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} {t('form.feedAlreadyExists')} row['urls.direct_download'] === feedUrl, - ); - return match ? match['id'] : null; + if ( + parsed.data == null || + !Array.isArray(parsed.data) || + !parsed.data.every((row) => typeof row === 'object' && row !== null) + ) { + return null; + } + interface FeedCsvRow { + 'urls.direct_download'?: string; + id?: string; + } + const rows = parsed.data as FeedCsvRow[]; + const match = rows.find((row) => row['urls.direct_download'] === feedUrl); + return typeof match?.id === 'string' ? match.id : null; } catch (e) { return null; } From 334c2afd76c7d2dde9f5126832d346603546f08a Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 12:14:07 -0500 Subject: [PATCH 13/22] fix linter errors --- web-app/src/app/services/feeds/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index 7d66bb7a2..8a3826485 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -170,7 +170,9 @@ export async function checkFeedUrlExistsInCsv( if ( parsed.data == null || !Array.isArray(parsed.data) || - !parsed.data.every((row) => typeof row === 'object' && row !== null) + !parsed.data.every( + (row: unknown) => typeof row === 'object' && row !== null, + ) ) { return null; } From c0b7de0dffdb72710db6067fa7182bee2cf3adea Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 12:27:29 -0500 Subject: [PATCH 14/22] fix linter errors --- web-app/src/app/services/feeds/utils.ts | 33 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index 8a3826485..4553d29a7 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -170,20 +170,37 @@ export async function checkFeedUrlExistsInCsv( if ( parsed.data == null || !Array.isArray(parsed.data) || - !parsed.data.every( - (row: unknown) => typeof row === 'object' && row !== null, - ) + !parsed.data.every(isFeedCsvRow) ) { return null; } - interface FeedCsvRow { - 'urls.direct_download'?: string; - id?: string; - } - const rows = parsed.data as FeedCsvRow[]; + const rows = parsed.data; const match = rows.find((row) => row['urls.direct_download'] === feedUrl); return typeof match?.id === 'string' ? match.id : null; } catch (e) { return null; } } + +/** + * Type guard to check if a row from the CSV matches the FeedCsvRow interface. + */ +interface FeedCsvRow { + 'urls.direct_download'?: string; + id: string; +} + +/** + * + * @param row CSV row + * @returns check if csv has valid column names + */ +function isFeedCsvRow(row: unknown): row is FeedCsvRow { + if (typeof row !== 'object' || row === null) return false; + const obj = row as Record; + return ( + typeof obj.id === 'string' && + (obj['urls.direct_download'] === undefined || + typeof obj['urls.direct_download'] === 'string') + ); +} From 1f2f118a84384b5390731454eb81799d1e9098c3 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 12:39:04 -0500 Subject: [PATCH 15/22] readd the 'at least 1 url' validation --- .../Form/SecondStepRealtime.tsx | 35 ++++++++++++++----- web-app/src/app/services/feeds/utils.ts | 4 +-- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index 5e129d543..964b91223 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -83,6 +83,29 @@ export default function FormSecondStepRT({ } }, [tripUpdates, vehiclePositions, serviceAlerts]); + const gtfsRtLinkValidation = ( + rtType: 'tu' | 'vp' | 'sa', + ): boolean | string => { + if (tripUpdates !== '' || vehiclePositions !== '' || serviceAlerts !== '') { + switch (rtType) { + case 'tu': + return tripUpdates !== '' + ? isValidFeedLink(tripUpdates) || t('form.errorUrl') + : true; + case 'vp': + return vehiclePositions !== '' + ? isValidFeedLink(vehiclePositions) || t('form.errorUrl') + : true; + case 'sa': + return serviceAlerts !== '' + ? isValidFeedLink(serviceAlerts) || t('form.errorUrl') + : true; + } + } else { + return t('form.atLeastOneRealtimeFeed'); + } + }; + return ( <> { - if (!isValidFeedLink(value ?? '')) { - return t('form.errorUrl'); - } + gtfsRtLinkValidation('sa'); const exists = await checkFeedUrlExistsInCsv(value ?? ''); if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; @@ -204,9 +225,7 @@ export default function FormSecondStepRT({ rules={{ required: t('form.feedLinkRequired'), validate: async (value) => { - if (!isValidFeedLink(value ?? '')) { - return t('form.errorUrl'); - } + gtfsRtLinkValidation('tu'); const exists = await checkFeedUrlExistsInCsv(value ?? ''); if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; @@ -297,9 +316,7 @@ export default function FormSecondStepRT({ rules={{ required: t('form.feedLinkRequired'), validate: async (value) => { - if (!isValidFeedLink(value ?? '')) { - return t('form.errorUrl'); - } + gtfsRtLinkValidation('vp'); const exists = await checkFeedUrlExistsInCsv(value ?? ''); if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index 4553d29a7..afd96332c 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -183,7 +183,7 @@ export async function checkFeedUrlExistsInCsv( } /** - * Type guard to check if a row from the CSV matches the FeedCsvRow interface. + * FeedCsvRow interface with fields */ interface FeedCsvRow { 'urls.direct_download'?: string; @@ -191,7 +191,7 @@ interface FeedCsvRow { } /** - * + * Type guard to check if a row from the CSV matches the FeedCsvRow interface. * @param row CSV row * @returns check if csv has valid column names */ From e7e636204f8be2331cb05cfcb2fefc76ee529a90 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 13:22:29 -0500 Subject: [PATCH 16/22] fix the 'at least 1 url' validation --- .../screens/FeedSubmission/Form/FirstStep.tsx | 2 +- .../Form/SecondStepRealtime.tsx | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx index 80915e442..d53a7f52d 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -292,7 +292,7 @@ export default function FormFirstStep({ error={errors.feedLink !== undefined} {...field} helperText={ - errors.feedLink?.message === 'string' && + typeof errors.feedLink?.message === 'string' && errors.feedLink?.message?.startsWith('Feed Exists:') ? ( {t('form.feedAlreadyExists')} diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx index 964b91223..ba27d0ebd 100644 --- a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -131,9 +131,11 @@ export default function FormSecondStepRT({ control={control} name='serviceAlerts' rules={{ - required: t('form.feedLinkRequired'), validate: async (value) => { - gtfsRtLinkValidation('sa'); + const atLeastOneFeed = gtfsRtLinkValidation('sa'); + if (atLeastOneFeed !== true) { + return atLeastOneFeed; + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; @@ -148,7 +150,7 @@ export default function FormSecondStepRT({ error={errors.serviceAlerts !== undefined} data-cy='serviceAlertFeed' helperText={ - errors.serviceAlerts?.message === 'string' && + typeof errors.serviceAlerts?.message === 'string' && errors.serviceAlerts?.message?.startsWith( 'Feed Exists:', ) ? ( @@ -223,9 +225,11 @@ export default function FormSecondStepRT({ control={control} name='tripUpdates' rules={{ - required: t('form.feedLinkRequired'), validate: async (value) => { - gtfsRtLinkValidation('tu'); + const atLeastOneFeed = gtfsRtLinkValidation('tu'); + if (atLeastOneFeed !== true) { + return atLeastOneFeed; + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; @@ -239,7 +243,7 @@ export default function FormSecondStepRT({ {...field} error={errors.tripUpdates !== undefined} helperText={ - errors.tripUpdates?.message === 'string' && + typeof errors.tripUpdates?.message === 'string' && errors.tripUpdates?.message?.startsWith( 'Feed Exists:', ) ? ( @@ -314,9 +318,11 @@ export default function FormSecondStepRT({ control={control} name='vehiclePositions' rules={{ - required: t('form.feedLinkRequired'), validate: async (value) => { - gtfsRtLinkValidation('vp'); + const atLeastOneFeed = gtfsRtLinkValidation('vp'); + if (atLeastOneFeed !== true) { + return atLeastOneFeed; + } const exists = await checkFeedUrlExistsInCsv(value ?? ''); if (typeof exists === 'string' && exists.length > 0) { return `Feed Exists:${exists}`; @@ -330,7 +336,7 @@ export default function FormSecondStepRT({ {...field} error={errors.vehiclePositions !== undefined} helperText={ - errors.vehiclePositions?.message === 'string' && + typeof errors.vehiclePositions?.message === 'string' && errors.vehiclePositions?.message?.startsWith( 'Feed Exists:', ) ? ( From ec4156edb5f1f43aab8fb71eacc4d5b90b0d9c37 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 13:56:04 -0500 Subject: [PATCH 17/22] fix linter error by specifying type for papaparse --- web-app/src/app/services/feeds/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index afd96332c..47347fb6c 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -166,7 +166,7 @@ export async function checkFeedUrlExistsInCsv( const response = await fetch(csvUrl); if (!response.ok) throw new Error('Failed to fetch CSV'); const csvText = await response.text(); - const parsed = Papa.parse(csvText, { header: true }); + const parsed = Papa.parse(csvText, { header: true }); if ( parsed.data == null || !Array.isArray(parsed.data) || From 6c56462f2eca54e0be60245dff454d2b07211e5b Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 14:00:00 -0500 Subject: [PATCH 18/22] fix linter errors by removing csv row type checking --- web-app/src/app/services/feeds/utils.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/web-app/src/app/services/feeds/utils.ts b/web-app/src/app/services/feeds/utils.ts index 47347fb6c..4e6ff7ec0 100644 --- a/web-app/src/app/services/feeds/utils.ts +++ b/web-app/src/app/services/feeds/utils.ts @@ -167,13 +167,7 @@ export async function checkFeedUrlExistsInCsv( if (!response.ok) throw new Error('Failed to fetch CSV'); const csvText = await response.text(); const parsed = Papa.parse(csvText, { header: true }); - if ( - parsed.data == null || - !Array.isArray(parsed.data) || - !parsed.data.every(isFeedCsvRow) - ) { - return null; - } + if (parsed.data == null || !Array.isArray(parsed.data)) return null; const rows = parsed.data; const match = rows.find((row) => row['urls.direct_download'] === feedUrl); return typeof match?.id === 'string' ? match.id : null; @@ -189,18 +183,3 @@ interface FeedCsvRow { 'urls.direct_download'?: string; id: string; } - -/** - * Type guard to check if a row from the CSV matches the FeedCsvRow interface. - * @param row CSV row - * @returns check if csv has valid column names - */ -function isFeedCsvRow(row: unknown): row is FeedCsvRow { - if (typeof row !== 'object' || row === null) return false; - const obj = row as Record; - return ( - typeof obj.id === 'string' && - (obj['urls.direct_download'] === undefined || - typeof obj['urls.direct_download'] === 'string') - ); -} From 382e9afaffd1d458d2e7c81e4ac15edad9985652 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 14:07:07 -0500 Subject: [PATCH 19/22] add papaparse dependency --- web-app/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-app/package.json b/web-app/package.json index c13243a15..cd00fcad5 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -51,7 +51,8 @@ "recharts": "^2.12.7", "redux-persist": "^6.0.0", "redux-saga": "^1.2.3", - "yup": "^1.3.2" + "yup": "^1.3.2", + "papaparse": "^5.5.3" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0", From 499b7a34228b5035cf7e1594230566d26a40bb03 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 14:09:00 -0500 Subject: [PATCH 20/22] remove papaparse dependency (in wrong place) --- functions/packages/feed-form/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/packages/feed-form/package.json b/functions/packages/feed-form/package.json index 17914a759..9a63ed25f 100644 --- a/functions/packages/feed-form/package.json +++ b/functions/packages/feed-form/package.json @@ -22,8 +22,7 @@ "firebase": "^10.6.0", "firebase-admin": "^11.8.0", "firebase-functions": "^4.3.1", - "google-spreadsheet": "^4.1.2", - "papaparse": "^5.5.3" + "google-spreadsheet": "^4.1.2" }, "devDependencies": { "@types/jest": "^29.5.8", From 1be0a22493deec78d2481ea49c6e97629663e4e6 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 14:16:08 -0500 Subject: [PATCH 21/22] update yarn.lock --- functions/packages/feed-form/package.json | 1 - web-app/yarn.lock | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/functions/packages/feed-form/package.json b/functions/packages/feed-form/package.json index 9a63ed25f..74758badc 100644 --- a/functions/packages/feed-form/package.json +++ b/functions/packages/feed-form/package.json @@ -26,7 +26,6 @@ }, "devDependencies": { "@types/jest": "^29.5.8", - "@types/papaparse": "^5.5.2", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "^5.12.0", "eslint": "^8.9.0", diff --git a/web-app/yarn.lock b/web-app/yarn.lock index e42184502..1a0360f96 100644 --- a/web-app/yarn.lock +++ b/web-app/yarn.lock @@ -11600,6 +11600,11 @@ pac-resolver@^7.0.0: ip "^1.1.8" netmask "^2.0.2" +papaparse@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.5.3.tgz#07f8994dec516c6dab266e952bed68e1de59fa9a" + integrity sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" From c07d2593b9d2245dfb731542d58bcdf6acdc8882 Mon Sep 17 00:00:00 2001 From: Ian Chan Date: Mon, 22 Dec 2025 17:13:01 -0500 Subject: [PATCH 22/22] move feed-form issue creation to separate PR --- .../feed-form/src/impl/feed-form-impl.ts | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/functions/packages/feed-form/src/impl/feed-form-impl.ts b/functions/packages/feed-form/src/impl/feed-form-impl.ts index 35e441dd4..f84b0f768 100644 --- a/functions/packages/feed-form/src/impl/feed-form-impl.ts +++ b/functions/packages/feed-form/src/impl/feed-form-impl.ts @@ -327,17 +327,7 @@ async function createGithubIssue( if (formData.country && formData.country in countries) { const country = countries[formData.country as TCountryCode]; const continent = continents[country.continent].toLowerCase(); - if (continent != null) labels.push(`region/${continent}`); - } - - if (formData.authType !== "None - 0") { - labels.push("auth required"); - } - - if (!isValidZipUrl(formData.feedLink)) { - if(!await isValidZipDownload(formData.feedLink)) { - labels.push("invalid"); - } + if (continent != null) labels.push(continent); } try { @@ -451,38 +441,3 @@ export function buildGithubIssueBody( return content; } /* eslint-enable */ - -/** - * Parses the provided URL to check if it is a valid ZIP file URL - * @param url The direct download URL provided in the feed form - * @returns {boolean} Whether the URL is a valid ZIP file URL - */ -function isValidZipUrl(url: string | undefined | null): boolean { - if (!url) return false; - try { - const parsed = new URL(url); - return parsed.pathname.toLowerCase().endsWith(".zip"); - } catch { - return false; - } -} - -/** - * Checks if the provided URL points to a valid ZIP file by making a HEAD request - * @param url The direct download URL provided in the feed form - * @returns {boolean} Whether the URL downloads a valid ZIP file - */ -async function isValidZipDownload(url: string | undefined | null): Promise { - try { - if (!url) return false; - const response = await axios.head(url, {maxRedirects: 2}); - const contentType = response.headers["content-type"]; - const contentDisposition = response.headers["content-disposition"]; - - if (contentType && contentType.includes("zip")) return true; - if (contentDisposition && contentDisposition.includes("zip")) return true; - return false; - } catch { - return false; - } -}