Compare commits

..

5 Commits

Author SHA1 Message Date
Wayne
a56df62099 chore(deps): Update dependencies across packages
This commit updates several dependencies in the frontend and backend packages.

- **Backend:**
  - Upgrades `xlsx` to version `0.20.3` by pointing to the official CDN URL. This ensures usage of the community edition with a permissive license.
  - Removes the unused `bull-board` development dependency.

- **Frontend:**
  - Upgrades `@sveltejs/kit` from `^2.16.0` to `^2.38.1` to stay current with the latest features and fixes.
2025-09-11 22:06:56 +03:00
Wei S.
26a760b232 Create FUNDING.yml (#102) 2025-09-10 17:09:13 +03:00
Wei S.
6be0774bc4 Display versions: Add new version notification in footer (#101)
* feat: Add new version notification in footer

This commit implements a system to check for new application versions and notify the user.

On page load, the server-side code now fetches the latest release from the GitHub repository API. It uses `semver` to compare the current application version with the latest release tag.

If a newer version is available, an alert is displayed in the footer with a link to the release page. The current application version is also now displayed in the footer. The version check is cached for one hour to minimize API requests.

* Modify version notification

* current version 0.3.1

* Resolve conflicts

* Code formatting

---------

Co-authored-by: Wayne <5291640+ringoinca@users.noreply.github.com>
2025-09-10 12:09:12 +03:00
Wei S.
4a23f8f29f feat: Add new version notification in footer (#99)
This commit implements a system to check for new application versions and notify the user.

On page load, the server-side code now fetches the latest release from the GitHub repository API. It uses `semver` to compare the current application version with the latest release tag.

If a newer version is available, an alert is displayed in the footer with a link to the release page. The current application version is also now displayed in the footer. The version check is cached for one hour to minimize API requests.

Co-authored-by: Wayne <5291640+ringoinca@users.noreply.github.com>
2025-09-09 23:36:35 +03:00
albanobattistella
074256ed59 Update it.json (#90) 2025-09-07 23:44:08 +03:00
19 changed files with 252 additions and 963 deletions

View File

@@ -52,19 +52,6 @@ STORAGE_S3_REGION=
# Set to 'true' for MinIO and other non-AWS S3 services
STORAGE_S3_FORCE_PATH_STYLE=false
# --- OCR Settings ---
# Enable or disable Optical Character Recognition for attachments.
# Default: false
OCR_ENABLED=true
# Comma-separated list of languages for OCR processing (e.g., eng,fra,deu,spa).
# These must correspond to the .traineddata files mounted in the TESSERACT_PATH directory.
# Default: "eng"
OCR_LANGUAGES="eng"
# The internal container path where Tesseract language data files (.traineddata) are located.
# This path is the target for the volume mount specified in docker-compose.yml.
# Default: "/opt/open-archiver/tessdata"
TESSERACT_PATH="/opt/open-archiver/tessdata"
# --- Security & Authentication ---
# Rate Limiting

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [wayneshn]

View File

@@ -11,9 +11,6 @@ services:
- .env
volumes:
- archiver-data:/var/data/open-archiver
# (Optional) Mount a host directory containing Tesseract language files for OCR.
# If you do not need OCR, you can safely comment out or remove the line below.
- ${TESSERACT_PATH:-./tessdata}:/opt/open-archiver/tessdata:ro
depends_on:
- postgres
- valkey

View File

@@ -1,58 +0,0 @@
# Attachment OCR
Open Archiver includes a powerful Optical Character Recognition (OCR) feature that allows it to extract text from images and scanned PDF documents during indexing. This makes the content of image-based attachments fully searchable.
## Overview
When enabled, the OCR service automatically processes common image formats and acts as a fallback for PDF files that do not contain selectable text. This is particularly useful for scanned documents, faxes, or photos of text.
## Enabling OCR
To enable the OCR feature, you must set the following environment variable in your `.env` file:
```ini
OCR_ENABLED=true
```
By default, this feature is disabled. If you do not need OCR, you can set this to `false` or omit the variable.
## Step-by-Step Language Configuration
The OCR service requires language data files to recognize text. You can add support for one or more languages by following these steps:
1. **Download Language Files**: Visit the official Tesseract `tessdata_fast` repository to find the available language files: [https://github.com/tesseract-ocr/tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast). Download the `.traineddata` file for each language you need (e.g., `fra.traineddata` for French, `deu.traineddata` for German).
2. **Create a Directory on Host**: On your **host machine** (the machine running Docker), create a directory at any location to store your language files. For example, `/opt/openarchiver/tessdata`.
3. **Add Language Files**: Place the downloaded `.traineddata` files into the directory you just created.
4. **Configure Paths and Languages in `.env`**: Update your `.env` file with the following variables:
- `TESSERACT_PATH`: Set this to the **full, absolute path** of the directory you created in Step 2.
- `OCR_LANGUAGES`: Set this to a comma-separated list of the language codes you downloaded.
```ini
# Example configuration in .env file
TESSERACT_PATH="/opt/openarchiver/tessdata"
OCR_LANGUAGES="eng,fra,deu"
```
## Docker Compose Configuration
The system uses a Docker volume to make the language files on your host machine available to the application inside the container. The `docker-compose.yml` file is already configured to use the `TESSERACT_PATH` variable from your `.env` file.
```yaml
services:
open-archiver:
# ... other settings
volumes:
- archiver-data:/var/data/open-archiver
# (Optional) Mount a host directory containing Tesseract language files for OCR.
# If you do not need OCR, you can safely comment out or remove the line below.
- ${TESSERACT_PATH:-./tessdata}:/opt/open-archiver/tessdata:ro
```
This line connects the host path specified in `TESSERACT_PATH` (defaulting to `./tessdata` if not set) to the fixed `/opt/open-archiver/tessdata` path inside the container. If you have disabled OCR, you can comment out or remove the volume mount line.
## Performance Note
OCR is a CPU-intensive process. To ensure the main application remains responsive, all OCR operations are handled by background workers. The number of concurrent OCR processes is automatically scaled based on the number of available CPU cores on your system.

View File

@@ -42,10 +42,6 @@ You must change the following placeholder values to secure your instance:
openssl rand -hex 32
```
### Attachment OCR Configuration
Open Archiver can extract text from images and scanned documents using Optical Character Recognition (OCR). For detailed instructions on how to enable and configure this feature, please see the [Attachment OCR Guide](../services/indexing-service/ocr.md).
### Storage Configuration
By default, the Docker Compose setup uses local filesystem storage, which is persisted using a Docker volume named `archiver-data`. This is suitable for most use cases.
@@ -107,14 +103,6 @@ These variables are used by `docker-compose.yml` to configure the services.
| `STORAGE_S3_REGION` | The region for S3-compatible storage (required if `STORAGE_TYPE` is `s3`). | |
| `STORAGE_S3_FORCE_PATH_STYLE` | Force path-style addressing for S3 (optional). | `false` |
#### OCR Settings
| Variable | Description | Default Value |
| ---------------- | --------------------------------------------------------------------------------------------- | ------------- |
| `OCR_ENABLED` | Enable or disable Optical Character Recognition for attachments. | `false` |
| `OCR_LANGUAGES` | A comma-separated list of languages for OCR processing (e.g., `eng,fra,deu`). | `eng` |
| `TESSERACT_PATH` | The path on the host machine where Tesseract language data files (`.traineddata`) are stored. | `./tessdata` |
#### Security & Authentication
| Variable | Description | Default Value |

View File

@@ -1,5 +1,6 @@
{
"name": "open-archiver",
"version": "0.3.1",
"private": true,
"scripts": {
"dev": "dotenv -- pnpm --filter \"./packages/*\" --parallel dev",

View File

@@ -50,7 +50,6 @@
"mammoth": "^1.9.1",
"meilisearch": "^0.51.0",
"multer": "^2.0.2",
"pdf-to-png-converter": "^3.7.1",
"pdf2json": "^3.1.6",
"pg": "^8.16.3",
"pino": "^9.7.0",
@@ -59,9 +58,8 @@
"pst-extractor": "^1.11.0",
"reflect-metadata": "^0.2.2",
"sqlite3": "^5.1.7",
"tesseract.js": "^6.0.1",
"tsconfig-paths": "^4.2.0",
"xlsx": "^0.18.5",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yauzl": "^3.2.0",
"zod": "^4.1.5"
},
@@ -76,7 +74,6 @@
"@types/multer": "^2.0.0",
"@types/node": "^24.0.12",
"@types/yauzl": "^2.10.3",
"bull-board": "^2.1.3",
"ts-node-dev": "^2.0.0",
"typescript": "^5.8.3"
}

View File

@@ -6,6 +6,4 @@ export const app = {
encryptionKey: process.env.ENCRYPTION_KEY,
isDemo: process.env.IS_DEMO === 'true',
syncFrequency: process.env.SYNC_FREQUENCY || '* * * * *', //default to 1 minute
ocrEnabled: process.env.OCR_ENABLED === 'true',
ocrLanguages: process.env.OCR_LANGUAGES || 'eng',
};

View File

@@ -1,88 +1,38 @@
import PDFParser from 'pdf2json';
import mammoth from 'mammoth';
import xlsx from 'xlsx';
import { ocrService } from '../services/OcrService';
import { logger } from '../config/logger';
import { config } from '../config';
import { pdfToPng } from 'pdf-to-png-converter';
interface PdfExtractResult {
text: string;
hasText: boolean;
}
function extractTextFromPdf(buffer: Buffer): Promise<PdfExtractResult> {
function extractTextFromPdf(buffer: Buffer): Promise<string> {
return new Promise((resolve) => {
const pdfParser = new PDFParser(null, true);
let completed = false;
const finish = (result: PdfExtractResult) => {
const finish = (text: string) => {
if (completed) return;
completed = true;
pdfParser.removeAllListeners();
resolve(result);
resolve(text);
};
pdfParser.on('pdfParser_dataError', (err) => {
logger.error({ err }, 'Error parsing PDF for text extraction');
finish({ text: '', hasText: false });
});
pdfParser.on('pdfParser_dataReady', (pdfData) => {
let hasText = false;
if (pdfData?.Pages) {
for (const page of pdfData.Pages) {
if (page.Texts && page.Texts.length > 0) {
hasText = true;
break;
}
}
}
const text = pdfParser.getRawTextContent();
finish({ text, hasText });
});
pdfParser.on('pdfParser_dataError', () => finish(''));
pdfParser.on('pdfParser_dataReady', () => finish(pdfParser.getRawTextContent()));
try {
pdfParser.parseBuffer(buffer);
} catch (err) {
logger.error({ err }, 'Error parsing PDF buffer');
finish({ text: '', hasText: false });
console.error('Error parsing PDF buffer', err);
finish('');
}
setTimeout(() => finish({ text: '', hasText: false }), 10000);
// Prevent hanging if the parser never emits events
setTimeout(() => finish(''), 10000);
});
}
const OCR_SUPPORTED_MIME_TYPES = [
'image/jpeg',
'image/png',
'image/tiff',
'image/bmp',
'image/webp',
'image/x-portable-bitmap',
];
export async function extractText(buffer: Buffer, mimeType: string): Promise<string> {
try {
if (mimeType === 'application/pdf') {
const pdfResult = await extractTextFromPdf(buffer);
if (!pdfResult.hasText && config.app.ocrEnabled) {
logger.info(
{ mimeType },
'PDF contains no selectable text. Attempting OCR fallback...'
);
const pngPages = await pdfToPng(buffer);
let ocrText = '';
for (const pngPage of pngPages) {
ocrText += await ocrService.recognize(pngPage.content) + '\n';
}
return ocrText;
}
return pdfResult.text;
}
if (OCR_SUPPORTED_MIME_TYPES.includes(mimeType) && config.app.ocrEnabled) {
return await ocrService.recognize(buffer);
return await extractTextFromPdf(buffer);
}
if (
@@ -111,10 +61,10 @@ export async function extractText(buffer: Buffer, mimeType: string): Promise<str
return buffer.toString('utf-8');
}
} catch (error) {
logger.error({ err: error, mimeType }, 'Error extracting text from attachment');
return '';
console.error(`Error extracting text from attachment with MIME type ${mimeType}:`, error);
return ''; // Return empty string on failure
}
logger.warn({ mimeType }, 'Unsupported MIME type for text extraction');
return '';
console.warn(`Unsupported MIME type for text extraction: ${mimeType}`);
return ''; // Return empty string for unsupported types
}

View File

@@ -1,71 +0,0 @@
import { createScheduler, createWorker, Scheduler } from 'tesseract.js';
import { config } from '../config';
import { logger } from '../config/logger';
class OcrService {
private static instance: OcrService;
private scheduler: Scheduler | null = null;
private isInitialized = false;
private constructor() { }
public static getInstance(): OcrService {
if (!OcrService.instance) {
OcrService.instance = new OcrService();
}
return OcrService.instance;
}
private async initialize(): Promise<void> {
if (this.isInitialized || !config.app.ocrEnabled) {
return;
}
logger.info({ languages: config.app.ocrLanguages }, 'Initializing OCR Service...');
this.scheduler = createScheduler();
const languages = config.app.ocrLanguages.split(',');
const numWorkers = Math.max(1, require('os').cpus().length - 1);
const workerPromises = Array.from({ length: numWorkers }).map(async () => {
const worker = await createWorker(languages, 1, {
cachePath: '/opt/open-archiver/tessdata',
});
this.scheduler!.addWorker(worker);
});
await Promise.all(workerPromises);
this.isInitialized = true;
logger.info(
`OCR Service initialized with ${numWorkers} workers for languages: [${languages.join(', ')}]`
);
}
public async recognize(buffer: Buffer): Promise<string> {
if (!config.app.ocrEnabled) return '';
if (!this.isInitialized) await this.initialize();
if (!this.scheduler) {
logger.error('OCR scheduler not available.');
return '';
}
try {
const {
data: { text },
} = await this.scheduler.addJob('recognize', buffer);
return text;
} catch (error) {
logger.error({ err: error }, 'Error during OCR processing');
return '';
}
}
public async terminate(): Promise<void> {
if (this.scheduler && this.isInitialized) {
logger.info('Terminating OCR Service...');
await this.scheduler.terminate();
this.scheduler = null;
this.isInitialized = false;
}
}
}
export const ocrService = OcrService.getInstance();

View File

@@ -1,8 +1,6 @@
import { Worker } from 'bullmq';
import { connection } from '../config/redis';
import indexEmailProcessor from '../jobs/processors/index-email.processor';
import { ocrService } from '../services/OcrService';
import { logger } from '../config/logger';
const processor = async (job: any) => {
switch (job.name) {
@@ -24,14 +22,7 @@ const worker = new Worker('indexing', processor, {
},
});
logger.info('Indexing worker started');
console.log('Indexing worker started');
const gracefulShutdown = async () => {
logger.info('Shutting down indexing worker...');
await worker.close();
await ocrService.terminate();
process.exit(0);
};
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', () => worker.close());
process.on('SIGTERM', () => worker.close());

View File

@@ -15,13 +15,14 @@
"dependencies": {
"@iconify/svelte": "^5.0.1",
"@open-archiver/types": "workspace:*",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/kit": "^2.38.1",
"bits-ui": "^2.8.10",
"clsx": "^2.1.1",
"d3-shape": "^3.2.0",
"jose": "^6.0.1",
"lucide-svelte": "^0.525.0",
"postal-mime": "^2.4.4",
"semver": "^7.7.2",
"svelte-persisted-store": "^0.12.0",
"sveltekit-i18n": "^2.4.2",
"tailwind-merge": "^3.3.1",
@@ -35,6 +36,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/d3-shape": "^3.1.7",
"@types/semver": "^7.7.1",
"dotenv": "^17.2.0",
"layerchart": "2.0.0-next.27",
"mode-watcher": "^1.1.0",

View File

@@ -52,16 +52,16 @@
<div class="mt-2 rounded-md border bg-white p-4">
{#if isLoading}
<p>{$t('components.email_preview.loading')}</p>
<p>{$t('app.components.email_preview.loading')}</p>
{:else if emailHtml}
<iframe
title={$t('archive.email_preview')}
title={$t('app.archive.email_preview')}
srcdoc={emailHtml()}
class="h-[600px] w-full border-none"
></iframe>
{:else if raw}
<p>{$t('components.email_preview.render_error')}</p>
<p>{$t('app.components.email_preview.render_error')}</p>
{:else}
<p class="text-gray-500">{$t('components.email_preview.not_available')}</p>
<p class="text-gray-500">{$t('app.components.email_preview.not_available')}</p>
{/if}
</div>

View File

@@ -1,18 +1,39 @@
<script lang="ts">
import { t } from '$lib/translations';
import * as Alert from '$lib/components/ui/alert';
import { Info } from 'lucide-svelte';
export let currentVersion: string;
export let newVersionInfo: { version: string; description: string; url: string } | null = null;
</script>
<footer class="bg-muted py-6 md:py-0">
<div
class="container mx-auto flex flex-col items-center justify-center gap-4 md:h-24 md:flex-row"
>
<div class="container mx-auto flex flex-col items-center justify-center gap-4 py-8 md:flex-row">
<div class="flex flex-col items-center gap-2">
{#if newVersionInfo}
<Alert.Root>
<Alert.Title class="flex items-center gap-2">
<Info class="h-4 w-4" />
{$t('app.components.footer.new_version_available')}
<a
href={newVersionInfo.url}
target="_blank"
class=" text-muted-foreground underline"
>
{newVersionInfo.description}
</a>
</Alert.Title>
</Alert.Root>
{/if}
<p class="text-balance text-center text-xs font-medium leading-loose">
© {new Date().getFullYear()}
<a href="https://openarchiver.com/" target="_blank">Open Archiver</a>. {$t(
'app.components.footer.all_rights_reserved'
)}
</p>
<p class="text-balance text-center text-xs font-medium leading-loose">
Version: {currentVersion}
</p>
</div>
</div>
</footer>

View File

@@ -163,7 +163,8 @@
"not_available": "Raw .eml file not available for this email."
},
"footer": {
"all_rights_reserved": "All rights reserved."
"all_rights_reserved": "All rights reserved.",
"new_version_available": "New version available"
},
"ingestion_source_form": {
"provider_generic_imap": "Generic IMAP",

View File

@@ -1,17 +1,17 @@
{
"app": {
"auth": {
"login": "Accesso",
"login": "Accedi",
"login_tip": "Inserisci la tua email qui sotto per accedere al tuo account.",
"email": "Email",
"password": "Password"
},
"common": {
"working": "In lavorazione"
"working": "In corso"
},
"archive": {
"title": "Archivio",
"no_subject": "Nessun oggetto",
"no_subject": "Nessun Oggetto",
"from": "Da",
"sent": "Inviato",
"recipients": "Destinatari",
@@ -20,27 +20,27 @@
"folder": "Cartella",
"tags": "Tag",
"size": "Dimensione",
"email_preview": "Anteprima email",
"email_preview": "Anteprima Email",
"attachments": "Allegati",
"download": "Scarica",
"actions": "Azioni",
"download_eml": "Scarica email (.eml)",
"delete_email": "Elimina email",
"email_thread": "Thread email",
"download_eml": "Scarica Email (.eml)",
"delete_email": "Elimina Email",
"email_thread": "Thread Email",
"delete_confirmation_title": "Sei sicuro di voler eliminare questa email?",
"delete_confirmation_description": "Questa azione non può essere annullata ed eliminerà permanentemente l'email e i suoi allegati.",
"delete_confirmation_description": "Questa azione non può essere annullata e rimuoverà permanentemente l'email e i suoi allegati.",
"deleting": "Eliminazione in corso",
"confirm": "Conferma",
"cancel": "Annulla",
"not_found": "Email non trovata."
},
"ingestions": {
"title": "Fonti di ingestione",
"ingestion_sources": "Fonti di ingestione",
"bulk_actions": "Azioni di massa",
"force_sync": "Forza sincronizzazione",
"title": "Sorgenti di Ingestione",
"ingestion_sources": "Sorgenti di Ingestione",
"bulk_actions": "Azioni di Massa",
"force_sync": "Forza Sincronizzazione",
"delete": "Elimina",
"create_new": "Crea nuovo",
"create_new": "Crea Nuovo",
"name": "Nome",
"provider": "Provider",
"status": "Stato",
@@ -52,28 +52,28 @@
"open_menu": "Apri menu",
"edit": "Modifica",
"create": "Crea",
"ingestion_source": "Fonte di ingestione",
"edit_description": "Apporta modifiche alla tua fonte di ingestione qui.",
"create_description": "Aggiungi una nuova fonte di ingestione per iniziare ad archiviare le email.",
"ingestion_source": "Sorgente di Ingestione",
"edit_description": "Apporta modifiche alla tua sorgente di ingestione qui.",
"create_description": "Aggiungi una nuova sorgente di ingestione per iniziare ad archiviare le email.",
"read": "Leggi",
"docs_here": "documenti qui",
"delete_confirmation_title": "Sei sicuro di voler eliminare questa ingestione?",
"delete_confirmation_description": "Questo eliminerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa ingestione. Se desideri solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa l'ingestione.",
"delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa ingestione. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi mettere in pausa l'ingestione.",
"deleting": "Eliminazione in corso",
"confirm": "Conferma",
"cancel": "Annulla",
"bulk_delete_confirmation_title": "Sei sicuro di voler eliminare {{count}} ingestioni selezionate?",
"bulk_delete_confirmation_description": "Questo eliminerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste ingestioni. Se desideri solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa le ingestioni."
"bulk_delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste ingestioni. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi mettere in pausa le ingestioni."
},
"search": {
"title": "Cerca",
"description": "Cerca email archiviate.",
"email_search": "Ricerca email",
"title": "Ricerca",
"description": "Ricerca email archiviate.",
"email_search": "Ricerca Email",
"placeholder": "Cerca per parola chiave, mittente, destinatario...",
"search_button": "Cerca",
"search_options": "Opzioni di ricerca",
"strategy_fuzzy": "Fuzzy",
"strategy_verbatim": "Verbatim",
"strategy_fuzzy": "Approssimativa",
"strategy_verbatim": "Esatta",
"strategy_frequency": "Frequenza",
"select_strategy": "Seleziona una strategia",
"error": "Errore",
@@ -87,18 +87,18 @@
"next": "Succ"
},
"roles": {
"title": "Gestione ruoli",
"role_management": "Gestione ruoli",
"create_new": "Crea nuovo",
"title": "Gestione Ruoli",
"role_management": "Gestione Ruoli",
"create_new": "Crea Nuovo",
"name": "Nome",
"created_at": "Creato il",
"actions": "Azioni",
"open_menu": "Apri menu",
"view_policy": "Visualizza policy",
"view_policy": "Visualizza Policy",
"edit": "Modifica",
"delete": "Elimina",
"no_roles_found": "Nessun ruolo trovato.",
"role_policy": "Policy ruolo",
"role_policy": "Policy Ruolo",
"viewing_policy_for_role": "Visualizzazione policy per il ruolo: {{name}}",
"create": "Crea",
"role": "Ruolo",
@@ -111,22 +111,22 @@
"cancel": "Annulla"
},
"system_settings": {
"title": "Impostazioni di sistema",
"system_settings": "Impostazioni di sistema",
"title": "Impostazioni di Sistema",
"system_settings": "Impostazioni di Sistema",
"description": "Gestisci le impostazioni globali dell'applicazione.",
"language": "Lingua",
"default_theme": "Tema predefinito",
"light": "Chiaro",
"dark": "Scuro",
"system": "Sistema",
"support_email": "Email di supporto",
"saving": "Salvataggio",
"save_changes": "Salva modifiche"
"support_email": "Email di Supporto",
"saving": "Salvataggio in corso",
"save_changes": "Salva Modifiche"
},
"users": {
"title": "Gestione utenti",
"user_management": "Gestione utenti",
"create_new": "Crea nuovo",
"title": "Gestione Utenti",
"user_management": "Gestione Utenti",
"create_new": "Crea Nuovo",
"name": "Nome",
"email": "Email",
"role": "Ruolo",
@@ -146,33 +146,10 @@
"confirm": "Conferma",
"cancel": "Annulla"
},
"setup": {
"title": "Configurazione",
"description": "Configura l'account amministratore iniziale per Open Archiver.",
"welcome": "Benvenuto",
"create_admin_account": "Crea il primo account amministratore per iniziare.",
"first_name": "Nome",
"last_name": "Cognome",
"email": "Email",
"password": "Password",
"creating_account": "Creazione account",
"create_account": "Crea account"
},
"layout": {
"dashboard": "Dashboard",
"ingestions": "Ingestioni",
"archived_emails": "Email archiviate",
"search": "Cerca",
"settings": "Impostazioni",
"system": "Sistema",
"users": "Utenti",
"roles": "Ruoli",
"logout": "Esci"
},
"components": {
"charts": {
"emails_ingested": "Email ingerite",
"storage_used": "Spazio di archiviazione utilizzato",
"emails_ingested": "Email Acquisite",
"storage_used": "Spazio di Archiviazione Utilizzato",
"emails": "Email"
},
"common": {
@@ -182,35 +159,36 @@
},
"email_preview": {
"loading": "Caricamento anteprima email...",
"render_error": "Impossibile visualizzare l'anteprima dell'email.",
"not_available": "File .eml non disponibile per questa email."
"render_error": "Impossibile renderizzare l'anteprima dell'email.",
"not_available": "File .eml grezzo non disponibile per questa email."
},
"footer": {
"all_rights_reserved": "Tutti i diritti riservati."
},
"ingestion_source_form": {
"provider_generic_imap": "IMAP generico",
"provider_generic_imap": "IMAP Generico",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "Importazione PST",
"provider_eml_import": "Importazione EML",
"select_provider": "Seleziona un provider",
"service_account_key": "Chiave account di servizio (JSON)",
"service_account_key_placeholder": "Incolla il contenuto JSON della tua chiave account di servizio",
"impersonated_admin_email": "Email amministratore impersonata",
"client_id": "ID applicazione (client)",
"client_secret": "Valore segreto client",
"client_secret_placeholder": "Inserisci il valore segreto, non l'ID segreto",
"tenant_id": "ID directory (tenant)",
"service_account_key": "Chiave Account di Servizio (JSON)",
"service_account_key_placeholder": "Incolla il contenuto JSON della chiave del tuo account di servizio",
"impersonated_admin_email": "Email dell'Amministratore Impersonato",
"client_id": "ID Applicazione (Client)",
"client_secret": "Valore Segreto Client",
"client_secret_placeholder": "Inserisci il Valore segreto, non l'ID Segreto",
"tenant_id": "ID Directory (Tenant)",
"host": "Host",
"port": "Porta",
"username": "Nome utente",
"username": "Nome Utente",
"use_tls": "Usa TLS",
"allow_insecure_cert": "Consenti certificato non sicuro",
"pst_file": "File PST",
"eml_file": "File EML",
"heads_up": "Attenzione!",
"org_wide_warning": "Si prega di notare che questa è un'operazione a livello di organizzazione. Questo tipo di ingestione importerà e indicizzerà <b>tutte</b> le caselle di posta elettronica della tua organizzazione. Se desideri importare solo caselle di posta elettronica specifiche, utilizza il connettore IMAP.",
"upload_failed": "Caricamento non riuscito, riprova"
"org_wide_warning": "Si prega di notare che questa è un'operazione a livello di organizzazione. Questo tipo di ingestione importerà e indicizzerà <b>tutte</b> le caselle di posta elettronica nella tua organizzazione. Se vuoi importare solo caselle di posta elettronica specifiche, usa il connettore IMAP.",
"upload_failed": "Caricamento Fallito, riprova"
},
"role_form": {
"policies_json": "Policy (JSON)",
@@ -223,28 +201,61 @@
"select_role": "Seleziona un ruolo"
}
},
"dashboard_page": {
"title": "Dashboard",
"meta_description": "Panoramica del tuo archivio email.",
"header": "Dashboard",
"create_ingestion": "Crea un'ingestione",
"no_ingestion_header": "Non hai alcuna fonte di ingestione configurata.",
"no_ingestion_text": "Aggiungi una fonte di ingestione per iniziare ad archiviare le tue caselle di posta.",
"total_emails_archived": "Email totali archiviate",
"total_storage_used": "Spazio di archiviazione totale utilizzato",
"failed_ingestions": "Ingestioni non riuscite (ultimi 7 giorni)",
"ingestion_history": "Cronologia ingestioni",
"no_ingestion_history": "Nessuna cronologia di ingestione disponibile.",
"storage_by_source": "Archiviazione per fonte di ingestione",
"no_ingestion_sources": "Nessuna fonte di ingestione disponibile.",
"indexed_insights": "Approfondimenti indicizzati",
"top_10_senders": "Top 10 mittenti",
"no_indexed_insights": "Nessun approfondimento indicizzato disponibile."
"setup": {
"title": "Configurazione",
"description": "Configura l'account amministratore iniziale per Open Archiver.",
"welcome": "Benvenuto",
"create_admin_account": "Crea il primo account amministratore per iniziare.",
"first_name": "Nome",
"last_name": "Cognome",
"email": "Email",
"password": "Password",
"creating_account": "Creazione Account",
"create_account": "Crea Account"
},
"layout": {
"dashboard": "Dashboard",
"ingestions": "Ingestioni",
"archived_emails": "Email archiviate",
"search": "Ricerca",
"settings": "Impostazioni",
"system": "Sistema",
"users": "Utenti",
"roles": "Ruoli",
"api_keys": "Chiavi API",
"logout": "Esci"
},
"api_keys_page": {
"title": "Chiavi API",
"header": "Chiavi API",
"generate_new_key": "Genera Nuova Chiave",
"name": "Nome",
"key": "Chiave",
"expires_at": "Scade il",
"created_at": "Creato il",
"actions": "Azioni",
"delete": "Elimina",
"no_keys_found": "Nessuna chiave API trovata.",
"generate_modal_title": "Genera Nuova Chiave API",
"generate_modal_description": "Fornisci un nome e una scadenza per la tua nuova chiave API.",
"expires_in": "Scade Tra",
"select_expiration": "Seleziona una scadenza",
"30_days": "30 Giorni",
"60_days": "60 Giorni",
"6_months": "6 Mesi",
"12_months": "12 Mesi",
"24_months": "24 Mesi",
"generate": "Genera",
"new_api_key": "Nuova Chiave API",
"failed_to_delete": "Impossibile eliminare la chiave API",
"api_key_deleted": "Chiave API eliminata",
"generated_title": "Chiave API Generata",
"generated_message": "La tua chiave API è stata generata, per favore copiala e salvala in un luogo sicuro. Questa chiave verrà mostrata solo una volta."
},
"archived_emails_page": {
"title": "Email archiviate",
"header": "Email archiviate",
"select_ingestion_source": "Seleziona una fonte di ingestione",
"header": "Email Archiviate",
"select_ingestion_source": "Seleziona una sorgente di ingestione",
"date": "Data",
"subject": "Oggetto",
"sender": "Mittente",
@@ -255,6 +266,24 @@
"no_emails_found": "Nessuna email archiviata trovata.",
"prev": "Prec",
"next": "Succ"
},
"dashboard_page": {
"title": "Dashboard",
"meta_description": "Panoramica del tuo archivio email.",
"header": "Dashboard",
"create_ingestion": "Crea un'ingestione",
"no_ingestion_header": "Non hai impostato nessuna sorgente di ingestione.",
"no_ingestion_text": "Aggiungi una sorgente di ingestione per iniziare ad archiviare le tue caselle di posta.",
"total_emails_archived": "Totale Email Archiviate",
"total_storage_used": "Spazio di Archiviazione Totale Utilizzato",
"failed_ingestions": "Ingestioni Fallite (Ultimi 7 Giorni)",
"ingestion_history": "Cronologia Ingestioni",
"no_ingestion_history": "Nessuna cronologia delle ingestioni disponibile.",
"storage_by_source": "Spazio di Archiviazione per Sorgente di Ingestione",
"no_ingestion_sources": "Nessuna sorgente di ingestione disponibile.",
"indexed_insights": "Approfondimenti indicizzati",
"top_10_senders": "I 10 Mittenti Principali",
"no_indexed_insights": "Nessun approfondimento indicizzato disponibile."
}
}
}

View File

@@ -3,6 +3,11 @@ import type { LayoutServerLoad } from './$types';
import 'dotenv/config';
import { api } from '$lib/server/api';
import type { SystemSettings } from '@open-archiver/types';
import { version } from '../../../../package.json';
import semver from 'semver';
let newVersionInfo: { version: string; description: string; url: string } | null = null;
let lastChecked: Date | null = null;
export const load: LayoutServerLoad = async (event) => {
const { locals, url } = event;
@@ -32,10 +37,35 @@ export const load: LayoutServerLoad = async (event) => {
? await systemSettingsResponse.json()
: null;
const now = new Date();
if (!lastChecked || now.getTime() - lastChecked.getTime() > 1000 * 60 * 60) {
try {
const res = await fetch(
'https://api.github.com/repos/LogicLabs-OU/OpenArchiver/releases/latest'
);
if (res.ok) {
const latestRelease = await res.json();
const latestVersion = latestRelease.tag_name.replace('v', '');
if (semver.gt(latestVersion, version)) {
newVersionInfo = {
version: latestVersion,
description: latestRelease.name,
url: latestRelease.html_url,
};
}
}
lastChecked = now;
} catch (error) {
console.error('Failed to fetch latest version from GitHub:', error);
}
}
return {
user: locals.user,
accessToken: locals.accessToken,
isDemo: process.env.IS_DEMO === 'true',
systemSettings,
currentVersion: version,
newVersionInfo: newVersionInfo,
};
};

View File

@@ -35,5 +35,5 @@
<main class="flex-1">
{@render children()}
</main>
<Footer />
<Footer currentVersion={data.currentVersion} newVersionInfo={data.newVersionInfo} />
</div>

667
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff