Skip to content

Commit c099d14

Browse files
authored
feat: improve data management (#1)
1 parent c985be1 commit c099d14

File tree

7 files changed

+170
-70
lines changed

7 files changed

+170
-70
lines changed

.gitattributes

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* text=auto eol=lf
2+
3+
*.html text eol=lf
4+
*.ini text eol=lf
5+
*.js text eol=lf
6+
*.json text eol=lf
7+
*.md text eol=lf
8+
*.vue text eol=lf
9+
*.ts text eol=lf
10+
11+
*.ico binary
12+
*.png binary

components/data/DownloadForm.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
v-model="formState.exchangeId"
1414
:items="downloadStore.exchanges"
1515
:placeholder="t('data.download.form.exchange.placeholder')"
16+
:loading="downloadStore.exchangesStatus === 'loading'"
17+
:disabled="downloadStore.exchangesStatus !== 'success'"
1618
/>
1719
</UFormField>
1820

@@ -87,6 +89,10 @@ const symbolsForSelectedExchange = computed(() => {
8789
return downloadStore.symbolsByExchange[formState.exchangeId]?.list ?? [];
8890
});
8991
92+
onMounted(() => {
93+
downloadStore.fetchExchanges();
94+
});
95+
9096
watch(() => formState.exchangeId, (newExchangeId) => {
9197
if (newExchangeId) {
9298
formState.symbols = []; // Reset symbols when exchange changes

components/data/ProgressList.vue

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,36 @@
22
<div v-if="downloadStore.downloadQueue.length > 0">
33
<h3 class="text-lg font-semibold mt-8 mb-2">{{ t('data.download.progress.title') }}</h3>
44
<UCard>
5-
<div class="space-y-4">
5+
<div class="space-y-6">
66
<div v-for="job in downloadStore.downloadQueue" :key="job.key">
77
<div class="flex justify-between items-center gap-4">
88
<div class="flex items-center gap-2 min-w-0">
99
<UIcon v-if="job.status === 'completed'" name="i-heroicons-check-circle-20-solid" class="text-green-500 w-5 h-5 flex-shrink-0" />
10-
<UIcon v-else-if="job.status === 'downloading'" name="i-heroicons-arrow-down-tray-20-solid" class="animate-pulse w-5 h-5 flex-shrink-0" />
10+
<UIcon v-else-if="job.status === 'downloading'" name="i-heroicons-arrow-down-tray-20-solid" class="w-5 h-5 flex-shrink-0" />
1111
<UIcon v-else-if="job.status === 'failed'" name="i-heroicons-exclamation-circle-20-solid" class="text-red-500 w-5 h-5 flex-shrink-0" />
12+
<UIcon v-else-if="job.status === 'cancelled'" name="i-heroicons-x-circle-20-solid" class="text-red-500 w-5 h-5 flex-shrink-0" />
1213
<UIcon v-else name="i-heroicons-pause-circle-20-solid" class="text-gray-400 w-5 h-5 flex-shrink-0" />
1314
<p class="text-sm truncate">
1415
<strong>{{ job.symbol }}</strong> [{{ job.timeframe }}]
15-
<span class="text-gray-500 dark:text-gray-400">- {{ job.message }}</span>
1616
</p>
1717
</div>
18-
<UProgress v-if="job.status === 'downloading'" :value="job.progress" class="w-32" />
18+
<UButton
19+
v-if="job.jobId && ['pending', 'downloading'].includes(job.status)"
20+
icon="i-heroicons-x-mark-20-solid"
21+
size="2xs"
22+
color="error"
23+
variant="soft"
24+
square
25+
class="cursor-pointer"
26+
:aria-label="`Cancel download for ${job.key}`"
27+
@click="downloadStore.cancelDownload(job.jobId)"
28+
/>
29+
</div>
30+
<div class="mt-1 space-y-1">
31+
<p class="text-xs text-gray-500 dark:text-gray-400" :class="{ 'text-center': job.status === 'downloading' }">
32+
{{ formatJobMessage(job.message) }}
33+
</p>
34+
<UProgress v-if="job.status === 'downloading'" v-model="job.progress" />
1935
</div>
2036
</div>
2137
</div>
@@ -26,4 +42,17 @@
2642
<script setup lang="ts">
2743
const { t } = useI18n();
2844
const downloadStore = useDataDownloadStore();
45+
46+
/**
47+
* Formats the job message to be more concise.
48+
* If the message contains "until", it extracts and displays only that part.
49+
* @param message The original message from the backend.
50+
*/
51+
const formatJobMessage = (message: string): string => {
52+
const untilIndex = message.toLowerCase().indexOf('up to');
53+
if (untilIndex !== -1) {
54+
return `Until ${message.slice(untilIndex + 6)}`;
55+
}
56+
return message;
57+
};
2958
</script>

components/view/DataManagement.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
</div>
5151
</div>
5252

53-
<UAlert v-else icon="i-heroicons-circle-stack" :title="t('data.management.empty')" />
53+
<UAlert v-else icon="i-heroicons-circle-stack" :title="t('data.management.empty')" color="neutral" variant="soft"/>
5454
</div>
5555
</div>
5656
</template>

stores/dataAvailabilityStore.ts

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,59 @@
1-
export const useDataAvailabilityStore = defineStore('dataAvailability', () => {
2-
const manifest = ref<DataAvailabilityManifestItem[]>([]);
3-
const status = ref<LoadingStatus>('idle');
4-
const toast = useToast();
5-
6-
const isLoading = computed(() => status.value === 'loading');
7-
8-
async function fetchManifest() {
9-
if (manifest.value.length > 0) return;
10-
11-
status.value = 'loading';
12-
try {
13-
manifest.value = await $fetch<DataAvailabilityManifestItem[]>('/api/data-availability');
14-
status.value = 'success';
15-
} catch (e: any) {
16-
toast.add({
17-
title: 'Error Loading Market Data',
18-
description: e.data?.error || 'Could not connect to the API to get available market data.',
19-
color: 'error',
20-
icon: 'i-heroicons-exclamation-triangle',
21-
});
22-
console.error('Failed to fetch data availability manifest:', e);
23-
status.value = 'error';
24-
}
25-
}
26-
27-
const availableSymbols = computed(() => manifest.value.map(item => item.symbol).sort());
28-
29-
const getTimeframesForSymbol = (symbol: string | null): TimeframeData[] => {
30-
if (!symbol) return [];
31-
const symbolData = manifest.value.find(item => item.symbol === symbol);
32-
return symbolData?.timeframes ?? [];
33-
};
34-
35-
const getDateRangeFor = (symbol: string | null, timeframe: string | null): { min?: string, max?: string } => {
36-
if (!symbol || !timeframe) return {};
37-
38-
const symbolTimeframes = getTimeframesForSymbol(symbol);
39-
const timeframeData = symbolTimeframes.find(tf => tf.timeframe === timeframe);
40-
41-
if (!timeframeData) return {};
42-
43-
return {
44-
min: timeframeData.startDate.split('T')[0],
45-
max: timeframeData.endDate.split('T')[0],
46-
};
47-
};
48-
49-
50-
return {
51-
manifest,
52-
status,
53-
isLoading,
54-
fetchManifest,
55-
availableSymbols,
56-
getTimeframesForSymbol,
57-
getDateRangeFor,
58-
};
59-
});
1+
export const useDataAvailabilityStore = defineStore('dataAvailability', () => {
2+
const manifest = ref<DataAvailabilityManifestItem[]>([]);
3+
const status = ref<LoadingStatus>('idle');
4+
const toast = useToast();
5+
6+
const isLoading = computed(() => status.value === 'loading');
7+
8+
async function fetchManifest(force = false) {
9+
if (manifest.value.length > 0 && !force) return;
10+
11+
status.value = 'loading';
12+
try {
13+
manifest.value = await $fetch<DataAvailabilityManifestItem[]>('/api/data-availability');
14+
status.value = 'success';
15+
} catch (e: any) {
16+
toast.add({
17+
title: 'Error Loading Market Data',
18+
description: e.data?.error || 'Could not connect to the API to get available market data.',
19+
color: 'error',
20+
icon: 'i-heroicons-exclamation-triangle',
21+
});
22+
console.error('Failed to fetch data availability manifest:', e);
23+
status.value = 'error';
24+
}
25+
}
26+
27+
const availableSymbols = computed(() => manifest.value.map(item => item.symbol).sort());
28+
29+
const getTimeframesForSymbol = (symbol: string | null): TimeframeData[] => {
30+
if (!symbol) return [];
31+
const symbolData = manifest.value.find(item => item.symbol === symbol);
32+
return symbolData?.timeframes ?? [];
33+
};
34+
35+
const getDateRangeFor = (symbol: string | null, timeframe: string | null): { min?: string, max?: string } => {
36+
if (!symbol || !timeframe) return {};
37+
38+
const symbolTimeframes = getTimeframesForSymbol(symbol);
39+
const timeframeData = symbolTimeframes.find(tf => tf.timeframe === timeframe);
40+
41+
if (!timeframeData) return {};
42+
43+
return {
44+
min: timeframeData.startDate.split('T')[0],
45+
max: timeframeData.endDate.split('T')[0],
46+
};
47+
};
48+
49+
50+
return {
51+
manifest,
52+
status,
53+
isLoading,
54+
fetchManifest,
55+
availableSymbols,
56+
getTimeframesForSymbol,
57+
getDateRangeFor,
58+
};
59+
});

stores/dataDownloadStore.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
export const useDataDownloadStore = defineStore('dataDownload', () => {
22
const toast = useToast();
33
const { public: { apiUrl } } = useRuntimeConfig();
4+
const dataAvailabilityStore = useDataAvailabilityStore();
45

56
// --- State ---
6-
const exchanges = ref(['okx', 'binance', 'bybit']); // Mocked exchange list
7+
const exchanges = ref<string[]>([]);
8+
const exchangesStatus = ref<LoadingStatus>('idle');
79
const symbolsByExchange = reactive<Record<string, { list: string[], status: LoadingStatus }>>({});
810
const downloadQueue = ref<DownloadJob[]>([]);
911
const isQueueRunning = ref(false);
1012

1113
// --- Actions ---
1214

1315
/**
14-
* Fetches (currently mocked) symbols for a given exchange.
16+
* Fetches the list of available exchanges from the API.
17+
*/
18+
async function fetchExchanges() {
19+
if (exchanges.value.length > 0 || exchangesStatus.value === 'loading') {
20+
return;
21+
}
22+
23+
exchangesStatus.value = 'loading';
24+
try {
25+
exchanges.value = await $fetch<string[]>('/api/data/exchanges');
26+
exchangesStatus.value = 'success';
27+
} catch (e: any) {
28+
exchangesStatus.value = 'error';
29+
toast.add({
30+
title: 'Error Fetching Exchanges',
31+
description: e.data?.error || 'Could not fetch the list of exchanges.',
32+
color: 'error',
33+
icon: 'i-heroicons-exclamation-triangle-20-solid',
34+
});
35+
}
36+
}
37+
38+
/**
39+
* Fetches symbols for a given exchange.
1540
*/
1641
async function fetchSymbols(exchangeId: string) {
1742
if (!exchangeId || symbolsByExchange[exchangeId]?.status === 'loading') {
@@ -107,6 +132,29 @@ export const useDataDownloadStore = defineStore('dataDownload', () => {
107132
}
108133
}
109134

135+
/**
136+
* Sends a cancellation request for a specific job.
137+
* @param jobId The ID of the job to cancel.
138+
*/
139+
async function cancelDownload(jobId: string) {
140+
try {
141+
await $fetch(`/api/data/download/${jobId}`, {
142+
method: 'DELETE'
143+
});
144+
toast.add({
145+
title: 'Cancellation Requested',
146+
description: `A request to cancel job ${jobId} has been sent.`,
147+
color: 'info'
148+
});
149+
} catch (e: any) {
150+
toast.add({
151+
title: 'Cancellation Error',
152+
description: e.data?.error || `Could not request cancellation for job ${jobId}.`,
153+
color: 'error'
154+
});
155+
}
156+
}
157+
110158
/**
111159
* Listens to a Mercure stream for a specific download job.
112160
*/
@@ -122,11 +170,12 @@ export const useDataDownloadStore = defineStore('dataDownload', () => {
122170
job.progress = data.progress || job.progress;
123171
job.message = data.message || job.message;
124172

125-
if (data.status === 'completed' || data.status === 'failed') {
173+
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
126174
job.status = data.status;
127175
eventSource.close();
128-
downloadQueue.value.shift(); // Remove completed/failed job
129-
processQueue(); // Process next in queue
176+
downloadQueue.value.shift();
177+
dataAvailabilityStore.fetchManifest(true);
178+
processQueue();
130179
resolve();
131180
}
132181
};
@@ -136,6 +185,7 @@ export const useDataDownloadStore = defineStore('dataDownload', () => {
136185
job.message = 'Connection to progress stream failed.';
137186
eventSource.close();
138187
downloadQueue.value.shift();
188+
dataAvailabilityStore.fetchManifest(true);
139189
processQueue();
140190
resolve();
141191
};
@@ -144,10 +194,13 @@ export const useDataDownloadStore = defineStore('dataDownload', () => {
144194

145195
return {
146196
exchanges,
197+
exchangesStatus,
147198
symbolsByExchange,
148199
downloadQueue,
149200
isQueueRunning,
201+
fetchExchanges,
150202
fetchSymbols,
151203
queueDownloads,
204+
cancelDownload,
152205
};
153206
});

types/DownloadJob.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ declare interface DownloadJob {
55
timeframe: string;
66
startDate: string;
77
endDate: string;
8-
status: 'pending' | 'downloading' | 'completed' | 'failed';
8+
status: 'pending' | 'downloading' | 'completed' | 'failed' | 'cancelled';
99
progress: number;
1010
message: string;
1111
jobId?: string; // Mercure Job ID

0 commit comments

Comments
 (0)