Skip to content

Latest commit

 

History

209 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

experience-ui

Reference web client for the OpenAgriNet Experience Layer. It talks to the Experience API, which in turn calls the Decision Support System (DSS). Its job is to make the DSS's capabilities visible — streamed answers, provenance, refusals, and the non-answered outcomes — in a deployment that carries no adopter branding.

Status: early. Forked 2026-09, under active refactoring. Nothing here is stable yet. See docs/ADR/0000-fork-oan-ui.md §6 for what came across from upstream.

Lineage

This repository is a hard fork of OpenAgriNet/OAN-UI, taken from branch bh-main at commit 50aa452 (2026-08-12), MIT licensed. The complete upstream history is preserved here and the fork point is tagged fork-point:

git log  fork-point..main    # everything this project changed
git diff fork-point..main

Upstream is a production tenant application. This repository exists because a reference implementation needs to be configurable and unbranded, which that application was never required to be. See docs/ADR/0000-fork-oan-ui.md for why we forked rather than built fresh, and what that decision costs.

What this is — and what it is not

It is a demonstration client: a neutral, configurable reference that shows what the Experience API and DSS can do, and a worked example for teams building their own client.

It is not a product, and it is not intended for adopters to deploy or fork as their branded farmer-facing app. It carries no tenant data, no production hardening, and no compatibility guarantees. Adopters should build their own client against the Experience API contract and treat this as a reference, not a starting point.

If you find yourself forking this to ship it, that is a signal the Experience API contract needs better documentation — please open an issue instead.

Quick start

Requires Node 22 or newer.

npm install
npm run dev      # http://localhost:3000

No environment variables are required to boot. Where the app sends API calls is api.baseUrl in config.json, an absolute path on the app's own origin. The default, /experience/api, is the reference deployment's path (see Serving from a sub-path).

The dev server proxies api.baseUrl to an Experience API on http://localhost:8078; set DEV_API_URL for another address. Without one, every question shows an error card.

To work with no API at all, set stubs.enabled to true in src/config/app-config.json. Every endpoint then returns canned data, nothing leaves the browser, and the chat works end to end — each reply is labelled as stubbed so fabricated agricultural advice cannot be mistaken for real. The shipped file is the deployed one, so do not commit the change. See src/lib/api-stubs.ts.

Git hooks

The hooks live in .husky/ and are activated by a single local git setting, core.hooksPath. Installing dependencies sets it, because package.json runs husky from its prepare script:

npm install

Check that yours are actually active:

git config core.hooksPath      # should print .husky/_

If that prints nothing, the hooks are not running. Fix it with npx husky. This is worth checking rather than assuming: core.hooksPath is local to your clone and is not committed, so anyone who installed dependencies before a hook existed silently has no hooks.

Hook Runs
pre-commit lint, typecheck, knip, tests
commit-msg checks the subject against the convention in CONVENTIONS.md

Hooks are fast feedback, not the gate — they can be skipped with --no-verify, so CI runs the same checks.

Deployment

The image is a static build served by nginx. It expects to sit behind something else — a reverse proxy terminating TLS and handling access control — and does nothing about either itself.

docker compose up --build        # http://localhost:8080

or without compose:

docker build -t experience-ui .
docker run -p 8080:8080 experience-ui

GET /healthz returns ok for whatever is in front.

Serving from a sub-path

The client is built for the origin root by default. To mount it under a path on a shared domain, build with that path and have the proxy strip the prefix:

docker build --build-arg VITE_BASE_PATH=/experience/ -t experience-ui .
location /experience/ {
    proxy_pass http://experience-ui:8080/;   # the trailing slash strips the prefix
}

location = /experience {
    return 301 /experience/;                 # otherwise a bare /experience 404s
}

The Experience API sits behind the same proxy, one level down. api.baseUrl in config.json defaults to /experience/api to match this layout, so nothing is mounted over the shipped config. The proxy routes that path to the API and strips the prefix:

location /experience/api/ {
    proxy_pass http://experience-api:8000/;   # your API's address; the slash strips the prefix
    proxy_buffering off;                      # or the answer arrives all at once
    proxy_read_timeout 120s;                  # the whole-turn timeout; nginx's default is 60s
    client_max_body_size 2m;                  # above the DSS's 1 MB, so the DSS decides "too long"
}

A deployment at another path changes api.baseUrl to match. Nothing derives it from VITE_BASE_PATH: the two can differ, and an API call that quietly went to the wrong place would be hard to trace.

Leading and trailing slashes on VITE_BASE_PATH both matter. The path is baked into the bundle, so an image built for a sub-path serves only from that path — a deployment that needs two paths builds two images. In CI it comes from the VITE_BASE_PATH repository variable, defaulting to /.

Note that nginx resolves a proxy_pass hostname once at config load: restart the app container and the proxy keeps the old address until it is reloaded too.

Brand assets in config.json

logo, favicon and assistantAvatar are used exactly as written — nothing rewrites them. Two forms work:

"logo": "brand/logo.png",                      // a file served by this app
"logo": "https://cdn.example.org/logo.svg"     // anywhere else

For a file, put it under public/ and reference it without a leading slash, so it resolves relative to wherever the app is served from. A leading slash pins it to the domain root and breaks a sub-path deployment.

Configuring a deployment

Everything a deployment changes — name, logo, favicon, colours, which features are on, whether stubs are enabled — lives in config.json, read at boot and applied without a rebuild. The image ships a default, so an unconfigured container runs.

To override it, mount a directory containing config.json at /etc/experience-ui:

docker run -p 8080:8080 -v "$PWD/my-config:/etc/experience-ui:ro" experience-ui

docker-compose.yml has the same mount commented out — uncomment it once the directory exists. It is left off by default because an empty or missing directory would shadow the config the image ships with.

# Kubernetes
volumeMounts:
  - name: config
    mountPath: /etc/experience-ui
volumes:
  - name: config
    configMap:
      name: experience-ui-config    # with a config.json key

A directory rather than a single file, deliberately: single-file bind mounts fail outright on some container runtimes, and a ConfigMap mounted with subPath never sees later updates.

config.json is served with Cache-Control: no-store, so a changed configuration takes effect on the next reload rather than whenever a cache happens to expire. Content-hashed assets under /assets/ are cached for a year; index.html is not cached, because it names those assets.

Documentation

Doc Contents
docs/ADR/ Accepted architecture decisions
AGENTS.md What this repo is, its stack, layout, testing patterns and gotchas
CONVENTIONS.md Naming, git workflow, commit format, linting

License

MIT — see LICENSE. The upstream copyright notice is retained as that license requires.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages