From 928d12df202ccecabeff125a798557607f7fa452 Mon Sep 17 00:00:00 2001 From: William Arin Date: Wed, 18 Jun 2025 21:03:53 +0800 Subject: [PATCH 1/3] feat: add all possible exchanges in the download form --- components/data/DownloadForm.vue | 6 ++++++ stores/dataDownloadStore.ts | 30 ++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/components/data/DownloadForm.vue b/components/data/DownloadForm.vue index 36b4afa..ad97155 100644 --- a/components/data/DownloadForm.vue +++ b/components/data/DownloadForm.vue @@ -13,6 +13,8 @@ v-model="formState.exchangeId" :items="downloadStore.exchanges" :placeholder="t('data.download.form.exchange.placeholder')" + :loading="downloadStore.exchangesStatus === 'loading'" + :disabled="downloadStore.exchangesStatus !== 'success'" /> @@ -87,6 +89,10 @@ const symbolsForSelectedExchange = computed(() => { return downloadStore.symbolsByExchange[formState.exchangeId]?.list ?? []; }); +onMounted(() => { + downloadStore.fetchExchanges(); +}); + watch(() => formState.exchangeId, (newExchangeId) => { if (newExchangeId) { formState.symbols = []; // Reset symbols when exchange changes diff --git a/stores/dataDownloadStore.ts b/stores/dataDownloadStore.ts index 723f3c7..e1526f8 100644 --- a/stores/dataDownloadStore.ts +++ b/stores/dataDownloadStore.ts @@ -3,7 +3,8 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { const { public: { apiUrl } } = useRuntimeConfig(); // --- State --- - const exchanges = ref(['okx', 'binance', 'bybit']); // Mocked exchange list + const exchanges = ref([]); + const exchangesStatus = ref('idle'); const symbolsByExchange = reactive>({}); const downloadQueue = ref([]); const isQueueRunning = ref(false); @@ -11,7 +12,30 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { // --- Actions --- /** - * Fetches (currently mocked) symbols for a given exchange. + * Fetches the list of available exchanges from the API. + */ + async function fetchExchanges() { + if (exchanges.value.length > 0 || exchangesStatus.value === 'loading') { + return; + } + + exchangesStatus.value = 'loading'; + try { + exchanges.value = await $fetch('/api/data/exchanges'); + exchangesStatus.value = 'success'; + } catch (e: any) { + exchangesStatus.value = 'error'; + toast.add({ + title: 'Error Fetching Exchanges', + description: e.data?.error || 'Could not fetch the list of exchanges.', + color: 'error', + icon: 'i-heroicons-exclamation-triangle-20-solid', + }); + } + } + + /** + * Fetches symbols for a given exchange. */ async function fetchSymbols(exchangeId: string) { if (!exchangeId || symbolsByExchange[exchangeId]?.status === 'loading') { @@ -144,9 +168,11 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { return { exchanges, + exchangesStatus, symbolsByExchange, downloadQueue, isQueueRunning, + fetchExchanges, fetchSymbols, queueDownloads, }; From 558538ac6021938c04a2ff1ffd3076b5fc840770 Mon Sep 17 00:00:00 2001 From: William Arin Date: Thu, 19 Jun 2025 21:06:11 +0800 Subject: [PATCH 2/3] feat: cancel button on market data download --- components/data/ProgressList.vue | 37 ++++++++++++++++++++++++++---- components/view/DataManagement.vue | 2 +- stores/dataDownloadStore.ts | 31 +++++++++++++++++++++---- types/DownloadJob.d.ts | 2 +- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/components/data/ProgressList.vue b/components/data/ProgressList.vue index b51e763..b93e69e 100644 --- a/components/data/ProgressList.vue +++ b/components/data/ProgressList.vue @@ -2,20 +2,36 @@

{{ t('data.download.progress.title') }}

-
+
- + +

{{ job.symbol }} [{{ job.timeframe }}] - - {{ job.message }}

- + +
+
+

+ {{ formatJobMessage(job.message) }} +

+
@@ -26,4 +42,17 @@ diff --git a/components/view/DataManagement.vue b/components/view/DataManagement.vue index 9983b66..386aa7e 100644 --- a/components/view/DataManagement.vue +++ b/components/view/DataManagement.vue @@ -50,7 +50,7 @@
- + diff --git a/stores/dataDownloadStore.ts b/stores/dataDownloadStore.ts index e1526f8..5b99808 100644 --- a/stores/dataDownloadStore.ts +++ b/stores/dataDownloadStore.ts @@ -125,12 +125,34 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { job.status = 'failed'; job.message = e.data?.error || 'Failed to start download.'; toast.add({ title: 'Download Error', description: job.message, color: 'error' }); - // Move to the next job even if this one fails to start downloadQueue.value.shift(); processQueue(); } } + /** + * Sends a cancellation request for a specific job. + * @param jobId The ID of the job to cancel. + */ + async function cancelDownload(jobId: string) { + try { + await $fetch(`/api/data/download/${jobId}`, { + method: 'DELETE' + }); + toast.add({ + title: 'Cancellation Requested', + description: `A request to cancel job ${jobId} has been sent.`, + color: 'info' + }); + } catch (e: any) { + toast.add({ + title: 'Cancellation Error', + description: e.data?.error || `Could not request cancellation for job ${jobId}.`, + color: 'error' + }); + } + } + /** * Listens to a Mercure stream for a specific download job. */ @@ -146,11 +168,11 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { job.progress = data.progress || job.progress; job.message = data.message || job.message; - if (data.status === 'completed' || data.status === 'failed') { + if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') { job.status = data.status; eventSource.close(); - downloadQueue.value.shift(); // Remove completed/failed job - processQueue(); // Process next in queue + downloadQueue.value.shift(); + processQueue(); resolve(); } }; @@ -175,5 +197,6 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { fetchExchanges, fetchSymbols, queueDownloads, + cancelDownload, }; }); diff --git a/types/DownloadJob.d.ts b/types/DownloadJob.d.ts index 95c1526..4339fda 100644 --- a/types/DownloadJob.d.ts +++ b/types/DownloadJob.d.ts @@ -5,7 +5,7 @@ declare interface DownloadJob { timeframe: string; startDate: string; endDate: string; - status: 'pending' | 'downloading' | 'completed' | 'failed'; + status: 'pending' | 'downloading' | 'completed' | 'failed' | 'cancelled'; progress: number; message: string; jobId?: string; // Mercure Job ID From ea3cf2ac0e72368bc748ae51f4973b0d72e05e44 Mon Sep 17 00:00:00 2001 From: William Arin Date: Thu, 19 Jun 2025 21:19:53 +0800 Subject: [PATCH 3/3] feat: update available local data when a download is completed --- .gitattributes | 12 ++++ stores/dataAvailabilityStore.ts | 118 ++++++++++++++++---------------- stores/dataDownloadStore.ts | 4 ++ 3 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3ed062e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto eol=lf + +*.html text eol=lf +*.ini text eol=lf +*.js text eol=lf +*.json text eol=lf +*.md text eol=lf +*.vue text eol=lf +*.ts text eol=lf + +*.ico binary +*.png binary diff --git a/stores/dataAvailabilityStore.ts b/stores/dataAvailabilityStore.ts index e57a836..1796ff0 100644 --- a/stores/dataAvailabilityStore.ts +++ b/stores/dataAvailabilityStore.ts @@ -1,59 +1,59 @@ -export const useDataAvailabilityStore = defineStore('dataAvailability', () => { - const manifest = ref([]); - const status = ref('idle'); - const toast = useToast(); - - const isLoading = computed(() => status.value === 'loading'); - - async function fetchManifest() { - if (manifest.value.length > 0) return; - - status.value = 'loading'; - try { - manifest.value = await $fetch('/api/data-availability'); - status.value = 'success'; - } catch (e: any) { - toast.add({ - title: 'Error Loading Market Data', - description: e.data?.error || 'Could not connect to the API to get available market data.', - color: 'error', - icon: 'i-heroicons-exclamation-triangle', - }); - console.error('Failed to fetch data availability manifest:', e); - status.value = 'error'; - } - } - - const availableSymbols = computed(() => manifest.value.map(item => item.symbol).sort()); - - const getTimeframesForSymbol = (symbol: string | null): TimeframeData[] => { - if (!symbol) return []; - const symbolData = manifest.value.find(item => item.symbol === symbol); - return symbolData?.timeframes ?? []; - }; - - const getDateRangeFor = (symbol: string | null, timeframe: string | null): { min?: string, max?: string } => { - if (!symbol || !timeframe) return {}; - - const symbolTimeframes = getTimeframesForSymbol(symbol); - const timeframeData = symbolTimeframes.find(tf => tf.timeframe === timeframe); - - if (!timeframeData) return {}; - - return { - min: timeframeData.startDate.split('T')[0], - max: timeframeData.endDate.split('T')[0], - }; - }; - - - return { - manifest, - status, - isLoading, - fetchManifest, - availableSymbols, - getTimeframesForSymbol, - getDateRangeFor, - }; -}); +export const useDataAvailabilityStore = defineStore('dataAvailability', () => { + const manifest = ref([]); + const status = ref('idle'); + const toast = useToast(); + + const isLoading = computed(() => status.value === 'loading'); + + async function fetchManifest(force = false) { + if (manifest.value.length > 0 && !force) return; + + status.value = 'loading'; + try { + manifest.value = await $fetch('/api/data-availability'); + status.value = 'success'; + } catch (e: any) { + toast.add({ + title: 'Error Loading Market Data', + description: e.data?.error || 'Could not connect to the API to get available market data.', + color: 'error', + icon: 'i-heroicons-exclamation-triangle', + }); + console.error('Failed to fetch data availability manifest:', e); + status.value = 'error'; + } + } + + const availableSymbols = computed(() => manifest.value.map(item => item.symbol).sort()); + + const getTimeframesForSymbol = (symbol: string | null): TimeframeData[] => { + if (!symbol) return []; + const symbolData = manifest.value.find(item => item.symbol === symbol); + return symbolData?.timeframes ?? []; + }; + + const getDateRangeFor = (symbol: string | null, timeframe: string | null): { min?: string, max?: string } => { + if (!symbol || !timeframe) return {}; + + const symbolTimeframes = getTimeframesForSymbol(symbol); + const timeframeData = symbolTimeframes.find(tf => tf.timeframe === timeframe); + + if (!timeframeData) return {}; + + return { + min: timeframeData.startDate.split('T')[0], + max: timeframeData.endDate.split('T')[0], + }; + }; + + + return { + manifest, + status, + isLoading, + fetchManifest, + availableSymbols, + getTimeframesForSymbol, + getDateRangeFor, + }; +}); diff --git a/stores/dataDownloadStore.ts b/stores/dataDownloadStore.ts index 5b99808..08dc113 100644 --- a/stores/dataDownloadStore.ts +++ b/stores/dataDownloadStore.ts @@ -1,6 +1,7 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { const toast = useToast(); const { public: { apiUrl } } = useRuntimeConfig(); + const dataAvailabilityStore = useDataAvailabilityStore(); // --- State --- const exchanges = ref([]); @@ -125,6 +126,7 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { job.status = 'failed'; job.message = e.data?.error || 'Failed to start download.'; toast.add({ title: 'Download Error', description: job.message, color: 'error' }); + // Move to the next job even if this one fails to start downloadQueue.value.shift(); processQueue(); } @@ -172,6 +174,7 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { job.status = data.status; eventSource.close(); downloadQueue.value.shift(); + dataAvailabilityStore.fetchManifest(true); processQueue(); resolve(); } @@ -182,6 +185,7 @@ export const useDataDownloadStore = defineStore('dataDownload', () => { job.message = 'Connection to progress stream failed.'; eventSource.close(); downloadQueue.value.shift(); + dataAvailabilityStore.fetchManifest(true); processQueue(); resolve(); };