Synaplan
開源 AI 平台,供企業使用:自行託管的 AI 代理、符合 GDPR 的聊天機器人小工具、RAG 知識庫、WhatsApp 與 Outlook AI、MCP、多模型路由。Docker、Kubernetes 或託管於 Germ
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Parts inventory: stock levels, locations, prices, suppliers, and their history.
Deliberately knows nothing about guide content, write-ups, or images. Those
live in ioref-web, a separate repository that joins to this one on
part_number over the API below. This app does hold the category a group
belongs to (Input, Output, Power...), because that is a fact about the group,
but the presentation of categories, colors, ordering, which ones to show, is
ioref-web's decision. That split is what lets this app be deployed by an
organization that has its own parts and no interest in ours.
uv sync
cp .env.example .env
uv run python manage.py migrate
uv run python manage.py createsuperuser
uv run python manage.py runserver
Admin at /admin/, API at /api/v1/.
docker compose up --build # or containerized
| Model | Purpose |
|---|---|
Part | Identity and stocking policy: number, name, unit, min/max, status |
Group | What kind of part it is. One per part |
Category | Where a group sits in the guides' nav (Input, Power...). Optional |
Tag | Cross-cutting facts that are not the type. Many per part |
Location | Where it lives. First-class, so an empty bin is a row |
StockEvent | Append-only count: (part, kind, quantity, observed_at) |
PriceObservation | Append-only price + supplier + purchase link |
Why append-only. A count is an observation, not a value to overwrite. Two people counting at once each add a row rather than racing to replace one, and a correction is a newer count, so what was believed and when survives.
Current values are derived (Part.on_floor, .total_on_hand, .needs_restock)
and annotated in list queries, so a page costs a constant number of queries.
Nothing stores a running total that could drift from the events under it.
A price, its supplier and its purchase link are one row, so a price is always attributable to who sold it at that price.
A part never counted reports None. That is not zero, and the difference
matters when deciding what to reorder.
/ serves read-only HTML: a searchable, filterable parts list and a per-part
page with count history. It exists so the application is useful on its own: a
deployment with no site in front of it still needs a way to look at its own stock.
Deliberately unbranded. House styling belongs in whatever site sits in front; ioref-web renders the same data in IDeATe's colors.
Prices and suppliers are shown only to signed-in users. Stock levels answer "do you have any, and where"; what it cost and who sold it is procurement's business. The view does not query them at all for anonymous visitors.
The exception is the CSV at /export.csv, which anyone can download. It
includes prices, suppliers and purchase links, by design.
Set PUBLIC_BROWSE=False to require a login for the HTML views entirely. The
API is unaffected either way.
/api/v1/, keyed on part_number.
| Method | Path | Scope |
|---|---|---|
GET | /parts/ | read |
GET | /parts/{part_number}/ | read |
GET | /parts/{part_number}/stock-events/ | read |
POST | /parts/{part_number}/stock-events/ | write |
GET | /parts/{part_number}/prices/ | read |
POST | /parts/{part_number}/prices/ | write |
GET | /locations/ | read |
GET | /groups/ | read |
GET | /categories/ | read |
GET | /tags/ | read |
GET | /health/ | none |
Filters on /parts/: status, group, category, tag, location,
search, needs_restock=1, part_number__in=a,b,c. /groups/ takes
category.
Group is not location. They are separate fields so a part can be reclassified without appearing to move, and moved without appearing to be reclassified.
Group holds only the fine level. Potentiometers, Capacitors, Diodes.
The macro level is Category on the group, optional and usually empty: most of
the catalog is stock rather than curriculum. What the categories look like,
and which are shown, is the guides site's decision.
Group is singular, tags are plural. A part is one kind of thing, which is
what makes ?group=capacitors an unambiguous answer to "every capacitor below
minimum". Tags carry what a part also is: 0386 is a soft potentiometer with
group=potentiometers and tag=touch.
Service callers send a bearer key:
Authorization: Bearer <prefix>.<secret>
Create one in the admin under API keys. The plaintext is displayed once at
creation and never again; only a SHA-256 hash is stored. Give ioref-web
a read key.
A leak of this repository or its database yields only hashes.
Scopes are read (safe methods only) and write.
AUTH_MODE selects the identity source; API keys are unaffected by it.
local: Django's own login. Development and small installs.shib: mod_shib terminates SAML upstream and passes attribute headers.oidc: Entra or any OIDC provider (uv add mozilla-django-oidc).Moving from Shibboleth to Entra is an env change plus a proxy reconfiguration, not an application change; no SAML or OIDC library is imported at module scope.
Accounts are never created by signing in. SSO asserts who someone is, never
that they may edit stock, and under shib the proxy admits everyone the
institution vouches for. An eppn with no account authenticates as nobody and the
request continues anonymously. Create accounts explicitly:
uv run python manage.py grant_staff [email protected] --superuser
uv run python manage.py grant_staff [email protected]
uv run python manage.py grant_staff [email protected] --revoke
Usernames are eppns, so a bare Andrew ID will never match the SSO headers.
--revoke withdraws access without deleting the account, because stock events
record who counted them.
AUTH_MODE=shibis only safe behind a proxy that overwrites the identity headers on every request. The app trusts them completely. If it is reachable without going through that proxy, anyone can authenticate as anyone by setting a header. See the deployment section ofCLAUDE.md.
django-unfold, themed in IDeATe's house green (#14B04D) as the admin
primary, over base grays that are the site's own neutral scale (#fdfdfd →
#1d1d1d) rather than Unfold's blue-tinted Tailwind slate. Typography is
Nunito Sans.
The stock column decorates only exceptions: crimson for below-minimum, gray for never-counted, plain number otherwise. Marking healthy rows too makes the list harder to scan and buries the ones needing work.
The scales live in UNFOLD["COLORS"]; a spin-out deployment replaces them to
rebrand. Category colors are not here at all: they belong to whatever renders
the categories, currently ioref-web.
uv run python manage.py test