Synaplan
기업을 위한 오픈소스 AI 플랫폼
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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