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/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/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/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 723f3c7..08dc113 100644
--- a/stores/dataDownloadStore.ts
+++ b/stores/dataDownloadStore.ts
@@ -1,9 +1,11 @@
export const useDataDownloadStore = defineStore('dataDownload', () => {
const toast = useToast();
const { public: { apiUrl } } = useRuntimeConfig();
+ const dataAvailabilityStore = useDataAvailabilityStore();
// --- 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 +13,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') {
@@ -107,6 +132,29 @@ export const useDataDownloadStore = defineStore('dataDownload', () => {
}
}
+ /**
+ * 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.
*/
@@ -122,11 +170,12 @@ 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();
+ dataAvailabilityStore.fetchManifest(true);
+ processQueue();
resolve();
}
};
@@ -136,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();
};
@@ -144,10 +194,13 @@ export const useDataDownloadStore = defineStore('dataDownload', () => {
return {
exchanges,
+ exchangesStatus,
symbolsByExchange,
downloadQueue,
isQueueRunning,
+ 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