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