From 8c81b6988849e170472b64f859d5751ae641c218 Mon Sep 17 00:00:00 2001 From: Esteban Date: Mon, 20 Jul 2026 18:48:17 -0500 Subject: [PATCH 01/14] add bible study filter system with responsive FiltersContainer and global useFilters composable --- .../searchPanel/FiltersContainer.vue | 106 +++++++ app/components/searchPanel/SearchPanel.vue | 197 ++++++------ app/composables/useFilters.ts | 73 +++++ app/utils/changelog.ts | 280 +++++++++--------- lang/en.json | 1 + lang/es.json | 1 + lang/fr.json | 1 + lang/pt.json | 1 + 8 files changed, 434 insertions(+), 226 deletions(-) create mode 100644 app/components/searchPanel/FiltersContainer.vue create mode 100644 app/composables/useFilters.ts diff --git a/app/components/searchPanel/FiltersContainer.vue b/app/components/searchPanel/FiltersContainer.vue new file mode 100644 index 0000000..8b68d01 --- /dev/null +++ b/app/components/searchPanel/FiltersContainer.vue @@ -0,0 +1,106 @@ + + + diff --git a/app/components/searchPanel/SearchPanel.vue b/app/components/searchPanel/SearchPanel.vue index 32a6b7c..0317795 100644 --- a/app/components/searchPanel/SearchPanel.vue +++ b/app/components/searchPanel/SearchPanel.vue @@ -2,6 +2,7 @@ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue' import { breakpointsTailwind, useDebounce } from '@vueuse/core' import PublicationDetail from '~/components/PublicationDetail.vue' +import FiltersContainer from '~/components/searchPanel/FiltersContainer.vue' import { useSettingsStore } from '~/stores/settings' interface Props { @@ -156,64 +157,15 @@ const sortMode = ref<'relevance' | 'date'>('relevance') // ---- Filtro bible_study multi-chip (solo para Estudios) -------------------- -interface BibleStudyChip { - id: number - title: string -} - -const bibleStudyInput = ref(null) -const activeBibleStudies = ref([]) -const isValidating = ref(false) - -async function applyBibleStudyFilter() { - const val = bibleStudyInput.value - if (val === null || val <= 0) return - if (activeBibleStudies.value.some(bs => bs.id === val)) { - toast.add({ title: 'Estudio ya agregado', description: `El estudio #${val} ya está en el filtro`, color: 'info' }) - bibleStudyInput.value = null - return - } - - isValidating.value = true - try { - const res = await documentsApi.multiSearch({ - multiSearchParameters: {}, - multiSearchSearchesParameter: { - searches: [{ - collection: props.mainCollection, - q: '*', - queryBy: 'title', - filterBy: `bible_study:=${val}`, - perPage: 1, - includeFields: 'bible_study,title' - }] - } - }) - - const hit = (res?.results?.[0] as { hits?: Array<{ document: { title?: string } }> })?.hits?.[0] - if (hit) { - activeBibleStudies.value = [...activeBibleStudies.value, { id: val, title: hit.document.title || '' }] - bibleStudyInput.value = null - refetchResults() - } else { - toast.add({ title: 'Estudio no encontrado', description: `No existe el estudio #${val}`, color: 'warning' }) - } - } catch (err) { - console.error('Error validando bible_study', err) - } finally { - isValidating.value = false - } -} - -function removeBibleStudyFilter(id: number) { - activeBibleStudies.value = activeBibleStudies.value.filter(bs => bs.id !== id) - refetchResults() -} - -function clearAllBibleStudyFilters() { - activeBibleStudies.value = [] - refetchResults() -} +const { + activeBibleStudies, + activeCount, + bibleStudyInput, + isValidating, + applyBibleStudyFilter, + removeBibleStudyFilter, + clearAllBibleStudyFilters, +} = useFilters() function refetchResults() { if (!debouncedQuery.value.trim()) { @@ -226,6 +178,10 @@ function refetchResults() { } } +watch(activeBibleStudies, () => { + refetchResults() +}) + // ---------------------------------------------------------------------------- const groupedHits = ref([]) @@ -690,6 +646,7 @@ function metaLocation(meta: DocMeta | undefined): string { {{ author }} +
{{ t('search.phrase') }}
-
- -
-
-
- - - + {{ $t('search.filter') }} - -
-
+ + +
+
+ +
+

Busqueda:

+ +
+ + + + + ('bible-study-chips', () => []) + +export function useFilters() { + const { documentsApi } = useTypesenseApi() + const toast = useToast() + + const bibleStudyInput = ref(null) + const isValidating = ref(false) + + const activeCount = computed(() => activeBibleStudies.value.length) + + async function applyBibleStudyFilter(mainCollection: string) { + const val = bibleStudyInput.value + if (val === null || val <= 0) return + if (activeBibleStudies.value.some(bs => bs.id === val)) { + toast.add({ title: 'Estudio ya agregado', description: `El estudio #${val} ya está en el filtro`, color: 'info' }) + bibleStudyInput.value = null + return + } + + isValidating.value = true + try { + const res = await documentsApi.multiSearch({ + multiSearchParameters: {}, + multiSearchSearchesParameter: { + searches: [{ + collection: mainCollection, + q: '*', + queryBy: 'title', + filterBy: `bible_study:=${val}`, + perPage: 1, + includeFields: 'bible_study,title' + }] + } + }) + + const hit = (res?.results?.[0] as { hits?: Array<{ document: { title?: string } }> })?.hits?.[0] + if (hit) { + activeBibleStudies.value = [...activeBibleStudies.value, { id: val, title: hit.document.title || '' }] + bibleStudyInput.value = null + } else { + toast.add({ title: 'Estudio no encontrado', description: `No existe el estudio #${val}`, color: 'warning' }) + } + } catch (err) { + console.error('Error validando bible_study', err) + } finally { + isValidating.value = false + } + } + + function removeBibleStudyFilter(id: number) { + activeBibleStudies.value = activeBibleStudies.value.filter(bs => bs.id !== id) + } + + function clearAllBibleStudyFilters() { + activeBibleStudies.value = [] + } + + return { + activeBibleStudies, + activeCount, + bibleStudyInput, + isValidating, + applyBibleStudyFilter, + removeBibleStudyFilter, + clearAllBibleStudyFilters + } +} diff --git a/app/utils/changelog.ts b/app/utils/changelog.ts index 45313f8..17e981b 100644 --- a/app/utils/changelog.ts +++ b/app/utils/changelog.ts @@ -6,141 +6,153 @@ export const typeConfig: Record Date: Mon, 20 Jul 2026 18:48:57 -0500 Subject: [PATCH 02/14] add deploy to actions on gitea --- .gitea/workflows/deploy.yml | 79 +++++++++++++++++++++++++++++++++++++ ecosystem.config.cjs | 25 ++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 ecosystem.config.cjs diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..906be45 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,79 @@ +name: Deploy Search Typesense + +on: + push: + branches: + - staging + - production + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + # Route each branch to a specific directory and PM2 environment + - name: Setup Environment Variables + run: | + if [[ "${{ github.ref_name }}" == "production" ]]; then + TARGET="/var/www/search.carpa.com" + echo "PM2_ENV=production" >> $GITHUB_ENV + elif [[ "${{ github.ref_name }}" == "staging" ]]; then + TARGET="/var/www/dev.search.carpa.com" + echo "PM2_ENV=staging" >> $GITHUB_ENV + fi + + PM2_NAME="search-${{ github.ref_name }}" + + echo "TARGET_DIR=$TARGET" >> $GITHUB_ENV + echo "PM2_NAME=$PM2_NAME" >> $GITHUB_ENV + + echo "NUXT_PUBLIC_FEEDBACK_WEBHOOK=${{ secrets.NUXT_PUBLIC_FEEDBACK_WEBHOOK }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ secrets.NUXT_PUBLIC_RECAPTCHA_SITE_KEY }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_FEEDBACK_MAX_PER_HOUR=${{ secrets.NUXT_PUBLIC_FEEDBACK_MAX_PER_HOUR }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_FEEDBACK_MAX_PER_SESSION=${{ secrets.NUXT_PUBLIC_FEEDBACK_MAX_PER_SESSION }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_FEEDBACK_COOLDOWN_SEC=${{ secrets.NUXT_PUBLIC_FEEDBACK_COOLDOWN_SEC }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_FEEDBACK_MIN_SECONDS=${{ secrets.NUXT_PUBLIC_FEEDBACK_MIN_SECONDS }}" >> $GITHUB_ENV + echo "NUXT_FEEDBACK_TOKEN=${{ secrets.NUXT_FEEDBACK_TOKEN }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_TYPESENSE_URL=${{ secrets.NUXT_PUBLIC_TYPESENSE_URL }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_TYPESENSE_API_KEY=${{ secrets.NUXT_PUBLIC_TYPESENSE_API_KEY }}" >> $GITHUB_ENV + echo "NUXT_PUBLIC_ENTRELINEAS_DEV_KEY=${{ secrets.NUXT_PUBLIC_ENTRELINEAS_DEV_KEY }}" >> $GITHUB_ENV + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Build Nuxt Application + run: npm run build + env: + NITRO_PRESET: node-server + + - name: Copy Build Files to Vultr + uses: appleboy/scp-action@master + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.VPS_SSH_KEY }} + port: 22 + source: ".output/*,package.json,ecosystem.config.cjs" + target: "${{ env.TARGET_DIR }}" + + - name: Restart PM2 on Vultr + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.VPS_SSH_KEY }} + port: 22 + script: | + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + cd ${{ env.TARGET_DIR }} + PM2_NAME=${{ env.PM2_NAME }} pm2 reload ${{ env.PM2_NAME }} --env ${{ env.PM2_ENV }} || \ + PM2_NAME=${{ env.PM2_NAME }} pm2 start ecosystem.config.cjs --env ${{ env.PM2_ENV }} diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..859264c --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,25 @@ +module.exports = { + apps: [ + { + // You can append the PM2_ENV dynamically in the script if you want unique names in the PM2 list, + // or rely on the directory path to keep them separate. + name: process.env.PM2_NAME || 'SearchTs', + // port: '3000', + // exec_mode: 'cluster', + // instances: 'max', + script: './.output/server/index.mjs', + + // Triggered by 'staging' branch + env_staging: { + NODE_ENV: 'staging', + PORT: 3005 + }, + + // Triggered by 'production' branch + env_production: { + NODE_ENV: 'production', + PORT: 3010 + } + } + ] +}; From 464b6d03588d0db7c56371a26311d613873b7c3b Mon Sep 17 00:00:00 2001 From: David Ascanio Date: Fri, 24 Jul 2026 15:30:54 -0300 Subject: [PATCH 03/14] optimization --- app/components/searchPanel/SearchPanel.vue | 531 ++++---------------- app/composables/useDocMetaCache.ts | 56 +++ app/composables/useDocumentDetailFetch.ts | 80 +++ app/composables/usePublicationFetch.ts | 46 +- app/composables/useTypesenseClient.ts | 9 + app/composables/useTypesenseSearch.ts | 539 +++++++++++++++++++++ app/pages/entrelineas.vue | 162 +------ app/plugins/typesense-client.ts | 24 + app/stores/history.ts | 5 +- docs/html-content-cleanup.md | 119 +++++ package.json | 3 +- scripts/benchmark-search.mjs | 202 ++++++++ 12 files changed, 1172 insertions(+), 604 deletions(-) create mode 100644 app/composables/useDocMetaCache.ts create mode 100644 app/composables/useDocumentDetailFetch.ts create mode 100644 app/composables/useTypesenseClient.ts create mode 100644 app/composables/useTypesenseSearch.ts create mode 100644 app/plugins/typesense-client.ts create mode 100644 docs/html-content-cleanup.md create mode 100644 scripts/benchmark-search.mjs diff --git a/app/components/searchPanel/SearchPanel.vue b/app/components/searchPanel/SearchPanel.vue index 32a6b7c..64e5138 100644 --- a/app/components/searchPanel/SearchPanel.vue +++ b/app/components/searchPanel/SearchPanel.vue @@ -1,6 +1,6 @@