Synaplan
기업을 위한 오픈소스 AI 플랫폼
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗An offline progressive web app for planning and logging belt quoting calls. Plan on a desktop, work on an Android phone, exchange files between the two.
Merges the Belt Call Log (phone call logging) and the Zone Call Planner (desktop scheduling) into one app. Both source projects stay frozen in their own repos.
See MANUAL.md for how to use it.
The app ships empty. Account names, contacts and the zone overrides are loaded on each device by the user. This repository is public, so:
Never commit a CRM export, a backup file, zone-overrides.json, the Zone Call Planner HTML, or the plant audit workbook.
Anyone can install this and load their own data.
No build step. No npm. Upload these nine files to the repository root:
| File | What it is |
|---|---|
index.html | Every screen, and all the CSS |
app.js | Everything else |
zones.js | The zone map — geography only, no customer names |
manuals.js | Offline engineering manual library, own database |
sw.js | Service worker, makes it work offline |
manifest.webmanifest | Makes it installable |
icon-192.png | Home screen icon |
icon-512.png | Home screen icon |
icon-maskable-512.png | Padded variant, survives launcher cropping |
Then: Settings → Pages → deploy from main, root folder. Open the URL in Chrome and Add to Home screen.
Filenames are case-sensitive on GitHub Pages, and the manifest must agree with what's actually in the repo.
sw.js line 6:
const CACHE = 'fieldcrm-v23';
Increment this whenever any file changes, or the old version is what gets tested and a fix will be reported as broken. This is the single most likely source of confusing behaviour after an update.
Eight files, no framework, no bundler. One external library: SheetJS from jsDelivr, for reading the Dynamics .xlsx. Cached by the service worker on first load.
Storage. IndexedDB, database fieldcrm, version 3.
| Store | Holds |
|---|---|
kv | Import metadata, zone overrides, territory weeks, manager reassignments, the OneDrive folder handle |
calls | Every call, open or closed, with photos as Blobs |
accounts | One record per account, keyed on the CRM account name |
appts | The schedule |
localStorage is used only for small preferences, all under an fcrm. prefix.
Storage names are load-bearing. GitHub Pages puts every repository on one origin, so this app shares an origin with the Belt Call Log. Database fieldcrm, cache prefix fieldcrm-, localStorage fcrm. — never the beltcall names.
Navigation uses history.pushState, so the Android back gesture moves back a screen instead of closing the app. Dialogs get their own history entry.
Unplanned calls create their own appointment, marked origin and acked:false. They travel to the other device in the call file's newAppts. The plan file is authoritative for its date range and deletes anything inside it that the file does not contain — so an appointment made here and never sent is exempt until the other side acknowledges it by including it in a plan. Without that exemption a plan arriving after an unplanned call would wipe it.
Appointment sync over GitHub. The schedule, and only the schedule, moves between devices through a private repository via the Contents API. acctKey() is a 64-bit FNV-1a of the account name — not a crypto hash, and honestly not one: with a thousand-odd names anyone holding the list could rebuild the mapping. What it does is keep names out of the file and out of git history, which is the actual requirement. slimAppt() sends id, key, type, date, time, duration, contact indices, status and agenda; callSummary, callId and acct never leave. ghCheckRepo() refuses a public repository before any write. A 409 on a stale sha re-reads, merges and retries once. Settings and token live in kv.github on the device.
Two exchange folders, not one. DIR_OUT is PC → Phone (the PC writes plans), DIR_IN is Phone → PC (the phone writes calls). Each folder is read only for the kind that belongs in it, so neither device re-imports what it just wrote. A single dirHandle from an earlier build migrates to DIR_OUT.
One call per account per week. Before a new call is created, the week is checked for an existing call at that account. If there is one it is offered; taking it resumes the same record and merges in any contacts picked this time. Cancelled and missed calls are not offered.
Load log. Every file in or out is recorded in kv.loadLog with filename, kind, detail and success, capped at 12 and rendered under Exchange. Refusals are logged too, marked failed.
Projects carry forward, they do not repeat. projectsHere() gathers project entries across every call at the account, keyed on a normalised name, newest sighting winning, filtered to non-terminal statuses. Taking one records fromStatus so the report reads the move; updating one already in the current call replaces it rather than appending. PROJECT_DONE holds the two terminal statuses.
Health checks read their own history. healthHistory() matches on a loosely normalised asset key across previous calls, requires three characters, and surfaces the most recent with a count of the rest. Severity is required, and health items sort Urgent, Plan, Monitor, blank in both the compiled notes and the invite body.
Belt reference data. Plant_Audit_Template_1.xlsm is read on the device into kv.beltref — every valid Series > Style > Material > Colour, the link geometry the width check needs, and the sprocket table. The belt form is driven entirely by it; without it the pickers are empty and the form says so. Ported from Belt Call Log v13, along with the usage ranking in kv.usage that floats the values you reach for most.
Manual library. manuals.js owns its own database, fieldcrmmanuals — deliberately not the Belt Call Log's beltmanuals. Both apps share an origin on GitHub Pages, so sharing the database would let either app clear pages the other relies on, and uninstalling either would take both. The manuals are loaded once per app. pdf.js comes from a CDN on first import only.
Share target. The manifest registers the app as an Android share target for .json, .xlsx and .csv. The service worker catches the POST to ./share-target, parks the file in a separate unversioned cache (fieldcrm-share) and redirects with a 303 so a reload cannot re-post. The page collects the file on boot, deletes it from the cache, and routes it. Every incoming file — shared, or chosen with the Receive button — goes through one function that sniffs content rather than filename, so a plan file renamed by OneDrive to plan (1).json still works.
Layout switches at 900px. Below that the app routes to Today and This Week; above, to the planner. The same breakpoint is used by the routing and the CSS so the two cannot disagree.
Thirteen harnesses plus a QA pass, all headless with jsdom and fake-indexeddb.
npm install jsdom fake-indexeddb xlsx
node qa.mjs # wiring, ids, assets, a full walkthrough, bad input
node test.mjs # ... through test18.mjs
Roughly 1,370 assertions. They cover storage, the importer, ICS output, the exchange merge, cadence, navigation and the compiled notes.
They cannot cover: the camera, the share sheet, whether Blobs really persist in IndexedDB (fake-indexeddb does not preserve Blob identity), showDirectoryPicker against a real folder, SheetJS at full scale, or anything Outlook actually does with an .ics.
.xlsx import won't work until jsDelivr is reachable once. .csv still will.zones.js.Follows the Intralox Global Brand Guidelines. Light layout ratio: white dominant, grey and cyan secondary, red used sparingly. Red is the primary action colour here — a deliberate departure from the Belt Call Log, which kept red off buttons entirely. The red box logo is the first-instance mark, embedded in index.html and in the compiled notes.
Type: Roboto, then Arial. No webfont is fetched — the app has to work offline.