V1.0: Class 1 fax — real-world 4-page send to external number confirmed

Core features:
- Class 1 T.30 protocol: full send/receive implementation
- HDLC: DLE-stuffing, FCS strip, USR5637 bit-reversal handling
- T.4 MH encoder/decoder (1728px A4 standard)
- Document pipeline: PDF (Ghostscript), PNG, TIFF input
- Width clamping: US Letter 1734px → 1728px fax standard
- Cover page: CJK rasterization (TW/CN/JP/EN), TIFF + HTML output
- OCR verification: Tesseract 5 with eng+chi_tra, CJK space-tolerant
- API server (axum): health, send, jobs, cover, retry, cancel
- Background worker: auto-poll queue, speed fallback, retry policy
- Modem detection, pool management

Real-world test results (2026-07-23):
- V90 → 25153038: 4 pages, V.17 12000 bps, 2:33 ✅
- USR5637 → 25153038: 4 pages, V.17 12000 bps, 2:26 ✅
- Both faxes confirmed received on remote machine

Tested: loopback (100% pixel match), multi-page, all input formats,
cover pages, OCR verify, API endpoints, worker processing.
13 unit tests pass, 0 new clippy warnings.
This commit is contained in:
Warren
2026-07-24 18:47:15 +08:00
parent d1e92b32fb
commit 55bca92691
155 changed files with 25024 additions and 916 deletions
+138
View File
@@ -0,0 +1,138 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { addressBookApi } from '../api/addressBook'
import type { Contact, Group, CreateContactRequest, UpdateContactRequest, CreateGroupRequest } from '../types/addressBook'
export const useAddressBookStore = defineStore('addressBook', () => {
const contacts = ref<Contact[]>([])
const groups = ref<Group[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchContacts(query?: string, category?: string) {
try {
loading.value = true
const response = await addressBookApi.getContacts({ query, category })
contacts.value = response.data.contacts
} catch (e) {
error.value = 'Failed to fetch contacts'
console.error(e)
} finally {
loading.value = false
}
}
async function createContact(data: CreateContactRequest) {
try {
loading.value = true
const response = await addressBookApi.createContact(data)
await fetchContacts()
return response.data
} catch (e) {
error.value = 'Failed to create contact'
console.error(e)
throw e
} finally {
loading.value = false
}
}
async function updateContact(id: string, data: UpdateContactRequest) {
try {
loading.value = true
const response = await addressBookApi.updateContact(id, data)
await fetchContacts()
return response.data
} catch (e) {
error.value = 'Failed to update contact'
console.error(e)
throw e
} finally {
loading.value = false
}
}
async function deleteContact(id: string) {
try {
loading.value = true
await addressBookApi.deleteContact(id)
await fetchContacts()
} catch (e) {
error.value = 'Failed to delete contact'
console.error(e)
throw e
} finally {
loading.value = false
}
}
async function fetchGroups() {
try {
loading.value = true
const response = await addressBookApi.getGroups()
groups.value = response.data.groups
} catch (e) {
error.value = 'Failed to fetch groups'
console.error(e)
} finally {
loading.value = false
}
}
async function createGroup(data: CreateGroupRequest) {
try {
loading.value = true
const response = await addressBookApi.createGroup(data)
await fetchGroups()
return response.data
} catch (e) {
error.value = 'Failed to create group'
console.error(e)
throw e
} finally {
loading.value = false
}
}
async function deleteGroup(id: string) {
try {
loading.value = true
await addressBookApi.deleteGroup(id)
await fetchGroups()
} catch (e) {
error.value = 'Failed to delete group'
console.error(e)
throw e
} finally {
loading.value = false
}
}
async function addContactToGroup(contactId: string, groupId: string) {
try {
loading.value = true
await addressBookApi.addContactToGroup(contactId, groupId)
} catch (e) {
error.value = 'Failed to add contact to group'
console.error(e)
throw e
} finally {
loading.value = false
}
}
return {
contacts,
groups,
loading,
error,
fetchContacts,
createContact,
updateContact,
deleteContact,
fetchGroups,
createGroup,
deleteGroup,
addContactToGroup,
}
})
+93
View File
@@ -0,0 +1,93 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { faxApi } from '../api/client'
import type { ServerStatus, FaxJob, ModemStatus } from '../types'
export const useFaxStore = defineStore('fax', () => {
const status = ref<ServerStatus | null>(null)
const jobs = ref<FaxJob[]>([])
const modems = ref<ModemStatus[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchStatus() {
try {
loading.value = true
const response = await faxApi.getStatus()
status.value = response.data
} catch (e) {
error.value = 'Failed to fetch status'
console.error(e)
} finally {
loading.value = false
}
}
async function fetchJobs() {
try {
loading.value = true
const response = await faxApi.getJobs()
jobs.value = response.data
} catch (e) {
error.value = 'Failed to fetch jobs'
console.error(e)
} finally {
loading.value = false
}
}
async function fetchModems() {
try {
loading.value = true
const response = await faxApi.getModems()
modems.value = response.data
} catch (e) {
error.value = 'Failed to fetch modems'
console.error(e)
} finally {
loading.value = false
}
}
async function sendFax(to_number: string, options: any = {}) {
try {
loading.value = true
const response = await faxApi.sendFax({ to_number, ...options })
await fetchJobs()
return response.data
} catch (e) {
error.value = 'Failed to send fax'
console.error(e)
throw e
} finally {
loading.value = false
}
}
async function cancelJob(id: string) {
try {
loading.value = true
await faxApi.cancelJob(id)
await fetchJobs()
} catch (e) {
error.value = 'Failed to cancel job'
console.error(e)
throw e
} finally {
loading.value = false
}
}
return {
status,
jobs,
modems,
loading,
error,
fetchStatus,
fetchJobs,
fetchModems,
sendFax,
cancelJob,
}
})