Compare commits
9 Commits
main
...
47b4b87851
| Author | SHA1 | Date | |
|---|---|---|---|
| 47b4b87851 | |||
| b4081cff3e | |||
|
|
35796c0c3b | ||
|
|
0351e5c7a6 | ||
|
|
3df1465bf5 | ||
|
|
dcc5191ba3 | ||
|
|
220d3dc5a6 | ||
|
|
fa7bbe3953 | ||
|
|
ed1c8f6c96 |
27
.gitattributes
vendored
27
.gitattributes
vendored
@@ -1,27 +0,0 @@
|
|||||||
# Copied verbatim from rig/.gitattributes, and deliberately duplicated rather
|
|
||||||
# than shared: rig/ must carry its own so it survives being handed over on its
|
|
||||||
# own, and spr had none at all despite shipping ctrl/*.sh and generating
|
|
||||||
# gen/<room>/ctrl/*.sh.
|
|
||||||
#
|
|
||||||
# Line endings are normalised to LF in the repository and on checkout, on every
|
|
||||||
# platform. Without this, a checkout on Windows/WSL rewrites files to CRLF and
|
|
||||||
# every one of them shows up as modified without anyone having touched it.
|
|
||||||
#
|
|
||||||
# For the scripts it is not cosmetic: a shell script with CRLF fails on Linux
|
|
||||||
# with `bad interpreter: /usr/bin/env bash^M`, which reads as a broken installer
|
|
||||||
# rather than a line-ending problem — the worst possible first impression on a
|
|
||||||
# machine where nothing has been proven yet.
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
*.sh text eol=lf
|
|
||||||
*.py text eol=lf
|
|
||||||
*.env text eol=lf
|
|
||||||
*.yaml text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
|
|
||||||
# Never touch binaries.
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.zip binary
|
|
||||||
*.tar binary
|
|
||||||
*.gz binary
|
|
||||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -9,36 +9,12 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
# Secrets. There was no rule here, which is how a real API key ended up tracked
|
|
||||||
# in station/tools/tester/.env. Templates still ship.
|
|
||||||
.env
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Node
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Built library bundles (regenerate with `pnpm build` in the package).
|
|
||||||
# The dist is what a container boots; it is an artifact, not source.
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Generated runnable instance (entirely gitignored - regenerate with build.py)
|
# Generated runnable instance (entirely gitignored - regenerate with build.py)
|
||||||
gen/
|
gen/
|
||||||
|
|
||||||
# Specs and sheets uploaded through shuntgen's UI. Inputs someone dropped in a
|
|
||||||
# browser, not source — and often a client's real data.
|
|
||||||
soleprint/station/tools/shuntgen/uploads/
|
|
||||||
|
|
||||||
# Room configurations (separate repo - contains credentials and room-specific data)
|
# Room configurations (separate repo - contains credentials and room-specific data)
|
||||||
# Keep cfg/standalone/ and cfg/sample/ as templates, ignore actual rooms
|
# Keep cfg/standalone/ and cfg/sample/ as templates, ignore actual rooms
|
||||||
cfg/amar/
|
cfg/amar/
|
||||||
cfg/dlt/
|
cfg/dlt/
|
||||||
# Add new rooms here as they are created
|
# Add new rooms here as they are created
|
||||||
# cfg/<room>/
|
# cfg/<room>/
|
||||||
|
|
||||||
# Client rigs. A rig is a copy of rig/ renamed after the environment it models,
|
|
||||||
# so its k8s files spell out a real architecture — exactly the thing that must
|
|
||||||
# not land here. They are versioned in their own repo.
|
|
||||||
#
|
|
||||||
# Anchored at the ROOT on purpose: a copy is a SIBLING of rig/, so a rule inside
|
|
||||||
# rig/.gitignore cannot see it.
|
|
||||||
*-rig/
|
|
||||||
|
|||||||
147
CLAUDE.md
147
CLAUDE.md
@@ -51,19 +51,20 @@ spr/
|
|||||||
│ └── amar/ # Amar room config
|
│ └── amar/ # Amar room config
|
||||||
│ ├── config.json
|
│ ├── config.json
|
||||||
│ ├── data/
|
│ ├── data/
|
||||||
│ ├── soleprint/ # Room overlay — merged over soleprint/ on build
|
│ ├── artery/ # Amar-specific (merged into output)
|
||||||
│ │ ├── artery/ # room shunts, pulses
|
│ │ └── shunts/amar/
|
||||||
│ │ │ └── shunts/amar/
|
│ ├── atlas/ # Amar-specific books
|
||||||
│ │ ├── atlas/ # room books
|
│ │ └── books/
|
||||||
│ │ │ └── books/
|
│ ├── station/ # Amar-specific tools config
|
||||||
│ │ ├── station/ # room tool configs
|
│ │ └── tools/datagen/
|
||||||
│ │ │ └── tools/datagen/
|
|
||||||
│ │ └── nginx/
|
|
||||||
│ ├── ctrl/ # Room lifecycle scripts (copied into gen/<room>/)
|
|
||||||
│ ├── link/ # Bridge to managed app
|
│ ├── link/ # Bridge to managed app
|
||||||
│ └── amar/ # The managed app itself
|
│ ├── soleprint/ # Soleprint docker config
|
||||||
|
│ ├── databrowse/
|
||||||
|
│ ├── tester/
|
||||||
|
│ ├── monitors/
|
||||||
|
│ └── models/
|
||||||
│
|
│
|
||||||
├── ctrl/ # Build/run scripts (see Build & Run)
|
├── ctrl/ # Build/run scripts
|
||||||
│
|
│
|
||||||
└── gen/ # Built instances (gitignored)
|
└── gen/ # Built instances (gitignored)
|
||||||
├── standalone/
|
├── standalone/
|
||||||
@@ -99,38 +100,26 @@ Each room in `cfg/` has:
|
|||||||
- `config.json` - Framework branding/terminology
|
- `config.json` - Framework branding/terminology
|
||||||
- `data/` - Data files (veins.json, shunts.json, etc.)
|
- `data/` - Data files (veins.json, shunts.json, etc.)
|
||||||
|
|
||||||
Room-specific system configs live under `cfg/<room>/soleprint/` and are merged over
|
Room-specific system configs (merged into output):
|
||||||
the core `soleprint/` tree at build time:
|
- `artery/` - Room-specific shunts, pulses
|
||||||
- `soleprint/artery/` - Room-specific shunts, pulses
|
- `atlas/` - Room-specific books
|
||||||
- `soleprint/atlas/` - Room-specific books
|
- `station/` - Room-specific tool configs (datagen, tester tests, etc.)
|
||||||
- `soleprint/station/` - Room-specific tool configs (datagen generators, tester tests)
|
|
||||||
|
|
||||||
A room's own lifecycle scripts live in `cfg/<room>/ctrl/` and land in
|
|
||||||
`gen/<room>/ctrl/` — that is what `make start <room>` dispatches to.
|
|
||||||
|
|
||||||
## Build & Run
|
## Build & Run
|
||||||
|
|
||||||
`make` is the front door — one target per `ctrl/` script, with the subcommand as an
|
|
||||||
argument (`make cluster up`, not `make cluster-up`). The logic lives in the scripts,
|
|
||||||
never in the Makefile. `make help` lists every target.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make # = make help
|
# Build
|
||||||
make build [room|all|models] # -> gen/<room>/ (default: standalone)
|
python build.py # -> gen/standalone/
|
||||||
make start [room] [-d] # dispatches to gen/<room>/ctrl/start.sh
|
python build.py --cfg amar # -> gen/amar/
|
||||||
make stop [room]
|
python build.py --all # -> all rooms
|
||||||
make cluster [up|down|status] # the shared `spr` kind cluster
|
|
||||||
make component [list|sync|watch|publish|diff]
|
|
||||||
make deploy [--build|--sync-only]
|
|
||||||
```
|
|
||||||
|
|
||||||
Every script stays runnable on its own — the standalone rule holds:
|
# Run bare-metal
|
||||||
|
cd gen/standalone && python run.py
|
||||||
|
|
||||||
```bash
|
# Using ctrl scripts
|
||||||
python build.py --cfg amar # -> gen/amar/
|
./ctrl/build.sh [room]
|
||||||
cd gen/standalone && python run.py # bare-metal
|
./ctrl/start.sh [room] [-d]
|
||||||
./ctrl/cluster.sh up # still runs directly; rig builds the cluster
|
./ctrl/stop.sh [room]
|
||||||
cd gen/<room> && ./ctrl/start.sh # each room owns its lifecycle scripts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding a New Room
|
## Adding a New Room
|
||||||
@@ -140,12 +129,12 @@ mkdir -p cfg/newroom/data
|
|||||||
cp cfg/standalone/config.json cfg/newroom/
|
cp cfg/standalone/config.json cfg/newroom/
|
||||||
cp -r cfg/standalone/data/* cfg/newroom/data/
|
cp -r cfg/standalone/data/* cfg/newroom/data/
|
||||||
|
|
||||||
# Add room-specific configs as needed (note the soleprint/ overlay level):
|
# Add room-specific configs as needed:
|
||||||
# cfg/newroom/soleprint/artery/shunts/...
|
# cfg/newroom/artery/shunts/...
|
||||||
# cfg/newroom/soleprint/atlas/books/...
|
# cfg/newroom/atlas/books/...
|
||||||
# cfg/newroom/soleprint/station/tools/datagen/<name>.py
|
# cfg/newroom/station/tools/...
|
||||||
|
|
||||||
make build newroom
|
python build.py --cfg newroom
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ports
|
## Ports
|
||||||
@@ -160,51 +149,10 @@ make build newroom
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| modelgen | Generate models from config |
|
| modelgen | Generate models from config |
|
||||||
| datagen | Generate test data (uses faker) |
|
| datagen | Generate test data (uses faker) |
|
||||||
| tester | HTTP contract test runner |
|
| tester | BDD/playwright test runner |
|
||||||
| graphgen | Generate navigable model graphs |
|
| graphgen | Generate navigable model graphs |
|
||||||
| databrowse | SQL data browser |
|
| databrowse | SQL data browser |
|
||||||
|
|
||||||
## Shared UI (`soleprint/common/ui`)
|
|
||||||
|
|
||||||
Upstream copy of the `soleprint-ui` package. `mpr/ui/framework` and
|
|
||||||
`meetus/ui/framework` are **downstream copies** synced by `ctrl/spr.py` — edit here,
|
|
||||||
never there, or the next sync overwrites the change.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd soleprint/common/ui
|
|
||||||
pnpm install && pnpm typecheck && pnpm test
|
|
||||||
pnpm build # -> dist/soleprint-ui.js + dist/style.css
|
|
||||||
```
|
|
||||||
|
|
||||||
**The theme ships with the bundle.** Every component styles itself with
|
|
||||||
`var(--surface-0)` and friends from `src/tokens.css`, so `src/index.ts` imports
|
|
||||||
`src/theme.css` (tokens + `base.css`). A bundle without it renders broken, not merely
|
|
||||||
unbranded. Retheme by overriding the variables in `tokens.css`; don't inline colours
|
|
||||||
in components.
|
|
||||||
|
|
||||||
`dist/` and `node_modules/` are gitignored — the dist is an artifact a container
|
|
||||||
boots, not source. To hand someone a running artifact without the sources:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python ctrl/spr.py publish soleprint-ui <dest> --dist # bundle + manifest only
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
**No test bodies are committed to core.** `soleprint/station/tools/tester/` ships the
|
|
||||||
base class, runner and UI; tests belong to a room at
|
|
||||||
`cfg/<room>/soleprint/station/tools/tester/tests/`.
|
|
||||||
|
|
||||||
Two rules, both in `tester/tests/test_template.py`:
|
|
||||||
|
|
||||||
1. **One suite, any environment** — a test states what the API promises, never who
|
|
||||||
implements it or where it runs.
|
|
||||||
2. **No helper framework tools** — stdlib `unittest` + `httpx`. No pytest fixtures,
|
|
||||||
no framework test client, no factories, no ORM or database access.
|
|
||||||
|
|
||||||
Django, where it appears, is an optional private DB editor via its admin — never the
|
|
||||||
framework, and never something a test reaches into.
|
|
||||||
|
|
||||||
## External Paths
|
## External Paths
|
||||||
|
|
||||||
| What | Path |
|
| What | Path |
|
||||||
@@ -213,35 +161,6 @@ framework, and never something a test reaches into.
|
|||||||
|
|
||||||
## Files Ignored
|
## Files Ignored
|
||||||
|
|
||||||
- `gen/` - Regenerate with `make build [room]`
|
- `gen/` - Regenerate with `python build.py`
|
||||||
- `dist/` - Build artifact; regenerate with `pnpm build`
|
|
||||||
- `node_modules/`
|
|
||||||
- `fails/`, `def/` - Drafts
|
- `fails/`, `def/` - Drafts
|
||||||
- `__pycache__/`, `.venv/`
|
- `__pycache__/`, `.venv/`
|
||||||
|
|
||||||
## Known Broken (found, not yet fixed)
|
|
||||||
|
|
||||||
- `tester/tests/example/test_health.py` imports `pytest`, which is neither a
|
|
||||||
dependency nor allowed here — `tests/README.md` says stdlib `unittest` and
|
|
||||||
`httpx`, no pytest. It fails to import, so `python -m tester discover` reports
|
|
||||||
it as `_FailedTest`. It also duplicates `test_template.py`, which is the
|
|
||||||
sanctioned example. Removing it is probably the fix; core ships no tests.
|
|
||||||
- Old vocabulary survives in prose — `album`, `larder`, `ward`, `nest` and
|
|
||||||
`pawprint` still appear in READMEs, `docs/`, and this file's history. Live code
|
|
||||||
is clean; the docs lag.
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- **A real API key was tracked** in `station/tools/tester/.env`. Untracked, `.env`
|
|
||||||
added to `.gitignore`, `.env.example` added. **The key is still in git history
|
|
||||||
and must be rotated** — untracking does not unpublish it.
|
|
||||||
- Client vocabulary is out of live code. `atlas/main.py` was written entirely as
|
|
||||||
`Album`/`larder`/`PAWPRINT_URL`; `databrowse` docs described a `larder/` the
|
|
||||||
code stopped using; `get-api-key.sh` defaulted to the client's database name.
|
|
||||||
- Two dead back-links, found while renaming: `atlas/main.py` passed
|
|
||||||
`pawprint_url` and `artery/index.html` read `pawprint_url`, while `run.py`
|
|
||||||
passes `soleprint_url`. Neither "← Soleprint" link had ever rendered.
|
|
||||||
- `atlas/main.py` fetched `/api/data/album`; `main.py` serves `/api/data/atlas`.
|
|
||||||
`get_data()` had been failing and returning empty lists.
|
|
||||||
- `tester/tests/_dev/test_health.py` imported a non-existent `..endpoints`. The
|
|
||||||
import was unused; removing it makes the module discoverable (2 tests).
|
|
||||||
|
|||||||
85
Makefile
85
Makefile
@@ -1,85 +0,0 @@
|
|||||||
# Thin control Makefile — one target per ctrl/ script, and the subcommand is an
|
|
||||||
# argument rather than a second target: `make cluster down`, not `make cluster-down`.
|
|
||||||
#
|
|
||||||
# The logic lives in the scripts, never here. Each target maps to exactly one
|
|
||||||
# file, and that file holds the variants:
|
|
||||||
#
|
|
||||||
# make cluster up -> ctrl/cluster.sh up
|
|
||||||
# make build amar -> ctrl/build.sh amar
|
|
||||||
#
|
|
||||||
# Bare words pass straight through. Anything starting with a dash would be
|
|
||||||
# swallowed by make itself, so pass those via ARGS instead:
|
|
||||||
#
|
|
||||||
# make component ARGS="publish soleprint-ui /tmp/out --dist"
|
|
||||||
# make deploy ARGS="--build"
|
|
||||||
#
|
|
||||||
# Every script stays runnable on its own (./ctrl/cluster.sh up still works, and each
|
|
||||||
# built room keeps its own gen/<room>/ctrl/*.sh) — the standalone rule holds, and
|
|
||||||
# this only saves typing.
|
|
||||||
#
|
|
||||||
# Start with: make build && make start
|
|
||||||
|
|
||||||
# The room to act on when none is named. Rooms live in cfg/ and build into gen/.
|
|
||||||
ROOM ?= standalone
|
|
||||||
PYTHON ?= python3
|
|
||||||
export PYTHON
|
|
||||||
|
|
||||||
# Words after the target become the script's subcommand. Make would otherwise
|
|
||||||
# treat them as goals of their own, so each gets a no-op rule.
|
|
||||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
|
||||||
ifneq ($(ARGS),)
|
|
||||||
# Declares each extra word as a target that does nothing: `:` is an empty rule
|
|
||||||
# body and `@` silences it. Without this, `make build sample` runs the build and
|
|
||||||
# then fails with "No rule to make target 'sample'", because make reads every
|
|
||||||
# word on the line as something it has been asked to build.
|
|
||||||
$(eval $(ARGS):;@:)
|
|
||||||
# ...and as PHONY, because some of those words name real directories. `cfg`,
|
|
||||||
# `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a
|
|
||||||
# target that is an existing directory already built — so `make build ctrl` ran
|
|
||||||
# the build and then printed "make: 'ctrl' is up to date". The empty rule above
|
|
||||||
# is not enough on its own; only .PHONY stops make consulting the filesystem.
|
|
||||||
.PHONY: $(ARGS)
|
|
||||||
endif
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
.PHONY: help build start stop dist theme docs cluster deploy component
|
|
||||||
|
|
||||||
help: ## list targets
|
|
||||||
@grep -hE '^[a-z]+:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
|
||||||
|
|
||||||
# ── rooms ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
build: ## build a room into gen/ [<room>|all|models]
|
|
||||||
bash ctrl/build.sh $(or $(ARGS),$(ROOM))
|
|
||||||
|
|
||||||
start: ## run a built room [<room>] [-d] [--build]
|
|
||||||
bash ctrl/start.sh $(or $(ARGS),$(ROOM))
|
|
||||||
|
|
||||||
stop: ## stop a running room [<room>]
|
|
||||||
bash ctrl/stop.sh $(or $(ARGS),$(ROOM))
|
|
||||||
|
|
||||||
dist: ## compile the plexus UIs to single files [<room>]
|
|
||||||
bash ctrl/dist.sh $(or $(ARGS),$(ROOM))
|
|
||||||
|
|
||||||
# ── theme ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
theme: ## ad-hoc pages [new|parts|bake|check|export|run FILE]
|
|
||||||
bash ctrl/theme.sh $(or $(ARGS),bake)
|
|
||||||
|
|
||||||
# ── docs ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
docs: ## documentation [serve [port]|graphs [theme]] (default serve)
|
|
||||||
bash ctrl/docs.sh $(or $(ARGS),serve)
|
|
||||||
|
|
||||||
# ── cluster ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
cluster: ## shared kind cluster [up|down|status] (default status)
|
|
||||||
bash ctrl/cluster.sh $(or $(ARGS),status)
|
|
||||||
|
|
||||||
# ── distribution ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
component: ## publish components [list|sync|watch|publish|diff]
|
|
||||||
$(PYTHON) ctrl/spr.py $(or $(ARGS),list)
|
|
||||||
|
|
||||||
deploy: ## push standalone to the server [--build|--sync-only]
|
|
||||||
bash ctrl/deploy.sh $(ARGS)
|
|
||||||
50
README.md
50
README.md
@@ -1,15 +1,51 @@
|
|||||||
# soleprint
|
# Soleprint
|
||||||
|
|
||||||
Development workflow platform. Wraps existing applications with tools, testing, and documentation — without touching source code.
|
Development workflow platform. Run, test, and document everything in one place.
|
||||||
|
|
||||||
*Cada paso deja huella.*
|
## Quick Start
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
# Build and run standalone
|
||||||
|
python build.py
|
||||||
|
cd gen/standalone && docker compose up
|
||||||
|
# Visit http://localhost:12000
|
||||||
|
|
||||||
|
# Build and run with room config
|
||||||
|
python build.py --cfg <room>
|
||||||
|
cd gen/<room>/soleprint && docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
python build.py # -> gen/standalone/
|
||||||
|
python build.py --cfg <room> # -> gen/<room>/
|
||||||
|
python build.py --all # -> all rooms
|
||||||
|
|
||||||
|
# Using ctrl scripts
|
||||||
|
./ctrl/build.sh # Build standalone
|
||||||
|
./ctrl/build.sh <room> # Build room
|
||||||
|
./ctrl/start.sh # Start standalone
|
||||||
|
./ctrl/start.sh <room> # Start room
|
||||||
|
./ctrl/stop.sh <room> # Stop
|
||||||
|
./ctrl/logs.sh <room> # View logs
|
||||||
|
|
||||||
## Docs
|
# Bare-metal dev (without Docker)
|
||||||
|
cd gen/standalone && python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a New Room
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docs && python -m http.server 8080
|
# 1. Create room config
|
||||||
|
mkdir -p cfg/myroom/data
|
||||||
|
cp cfg/standalone/config.json cfg/myroom/
|
||||||
|
cp -r cfg/standalone/data/* cfg/myroom/data/
|
||||||
|
|
||||||
|
# 2. Build and run
|
||||||
|
python build.py --cfg myroom
|
||||||
|
cd gen/myroom/soleprint && docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
Visit `http://localhost:8080`. Start with [Concepts](docs/data/en/concepts.md) for the mental model, then [Quickstart](docs/data/en/quickstart.md) to run your first room.
|
Or use the generation UI: `python -m http.server 8080` in `spr/` and visit `/generate.html`.
|
||||||
|
|||||||
21
berth/.gitignore
vendored
21
berth/.gitignore
vendored
@@ -1,21 +0,0 @@
|
|||||||
# Machine-local config and credentials. Never committed.
|
|
||||||
ctrl/.env
|
|
||||||
|
|
||||||
# Rendered output. Regenerate with `make services render <target>`.
|
|
||||||
# Generated config is an artifact, not source — the estate file is the source.
|
|
||||||
#
|
|
||||||
# The path is ctrl/render/out/, NOT render/out/. A pattern containing a slash is
|
|
||||||
# anchored to the directory holding this .gitignore, so `render/out/` would mean
|
|
||||||
# berth/render/out/ — which does not exist, and the real output would have been
|
|
||||||
# committed. Caught by `git check-ignore -v`, which is the only way to be sure.
|
|
||||||
ctrl/render/out/
|
|
||||||
|
|
||||||
# The "default" scratch bucket: always gitignored, never versioned.
|
|
||||||
def/
|
|
||||||
|
|
||||||
# Key material. NEVER committed.
|
|
||||||
#
|
|
||||||
# Anchored to ctrl/ for the same reason as render/out/ above: a pattern with a
|
|
||||||
# slash resolves against this file's own directory. `git check-ignore -v` is the
|
|
||||||
# only way to confirm it, and ctrl/vpn.sh refuses to write a key until it does.
|
|
||||||
ctrl/.secrets/
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# One target per ctrl/ script; the subcommand is an argument, not a second
|
|
||||||
# target: `make estate show`, not `make estate-show`. The logic lives in the
|
|
||||||
# scripts, never here.
|
|
||||||
#
|
|
||||||
# Config layers, weakest first: ctrl/versions.env < ctrl/env.d/<target>.env <
|
|
||||||
# ctrl/.env < the environment. So `make estate plan TARGET=gcp` beats all.
|
|
||||||
#
|
|
||||||
# Every target defaults to its READ-ONLY verb, and the verbs that change a live
|
|
||||||
# estate are not reachable by a bare word. Rationale: README.md.
|
|
||||||
|
|
||||||
ESTATE := $(or $(shell sed -n 's/^ESTATE=//p' ctrl/.env 2>/dev/null),$(shell ls estate/*.json 2>/dev/null | head -1 | xargs -r basename | sed 's/\.json$$//'))
|
|
||||||
TARGET := $(or $(shell sed -n 's/^TARGET=//p' ctrl/.env 2>/dev/null),aws)
|
|
||||||
|
|
||||||
.PHONY: help check selftest estate services vpn dns certs host ports registry docs
|
|
||||||
|
|
||||||
help: ## list targets
|
|
||||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
|
||||||
|
|
||||||
# ── preflight ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
check: ## is this estate coherent? reports, never fixes
|
|
||||||
bash ctrl/check.sh
|
|
||||||
|
|
||||||
selftest: ## does berth still do what it says? exits 1 if not
|
|
||||||
bash ctrl/selftest.sh
|
|
||||||
|
|
||||||
ports: ## port map [show|verify] (default show)
|
|
||||||
bash ctrl/ports.sh $(or $(ARGS),show)
|
|
||||||
|
|
||||||
# ── the estate ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
estate: ## the estate [show|list|plan|apply|destroy] (default show)
|
|
||||||
bash ctrl/estate.sh $(or $(ARGS),show)
|
|
||||||
|
|
||||||
services: ## gateway routes [list|render <target>|deploy] (default list)
|
|
||||||
bash ctrl/services.sh $(or $(ARGS),list)
|
|
||||||
|
|
||||||
# ── the network ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
vpn: ## overlays [list|show <ov>|check|render|keygen] (default list)
|
|
||||||
bash ctrl/vpn.sh $(or $(ARGS),list)
|
|
||||||
|
|
||||||
# ── names and trust ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
dns: ## DNS records [list|add|add-wildcard|remove] (default list)
|
|
||||||
bash ctrl/dns.sh $(or $(ARGS),list)
|
|
||||||
|
|
||||||
certs: ## TLS [status|verify|renew|push] (default status)
|
|
||||||
bash ctrl/certs.sh $(or $(ARGS),status)
|
|
||||||
|
|
||||||
# ── the box ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
host: ## the remote box [status|ports|services] (default status)
|
|
||||||
bash ctrl/host.sh $(or $(ARGS),status)
|
|
||||||
|
|
||||||
registry: ## the image registry [status] (default status)
|
|
||||||
bash ctrl/registry.sh $(or $(ARGS),status)
|
|
||||||
|
|
||||||
# ── docs ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
docs: ## documentation [serve|graphs] (default serve)
|
|
||||||
bash ctrl/docs.sh $(or $(ARGS),serve)
|
|
||||||
|
|
||||||
# ── swallowing the argument words — MUST BE LAST IN THIS FILE ──────────────
|
|
||||||
#
|
|
||||||
# Words after the target are arguments, but make reads each as a goal, so each
|
|
||||||
# gets a no-op rule. This block must come AFTER the real targets: when an
|
|
||||||
# argument names one (`make host ports`, `make vpn check`), the last definition
|
|
||||||
# wins, and it has to be the no-op. With it first, make ran both scripts.
|
|
||||||
#
|
|
||||||
# Make's "overriding recipe" warning is the swallow working as intended.
|
|
||||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
|
||||||
ifneq ($(ARGS),)
|
|
||||||
$(eval $(ARGS):;@:)
|
|
||||||
# .PHONY too: some of those words name real directories (ctrl, estate, render),
|
|
||||||
# and make treats an existing directory as already built.
|
|
||||||
.PHONY: $(ARGS)
|
|
||||||
endif
|
|
||||||
250
berth/README.md
250
berth/README.md
@@ -1,250 +0,0 @@
|
|||||||
# berth
|
|
||||||
|
|
||||||
spr's deploy half. A rig is a mobile installation; a **berth** is the allocated, paid-for
|
|
||||||
place where it is moored and actually operates. Local rig → remote berth.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make check # is this estate coherent? reports, never fixes
|
|
||||||
make estate show # the description, resolved
|
|
||||||
make vpn check # the overlay: addresses, routing, bindings, key hygiene
|
|
||||||
make services render aws
|
|
||||||
make ports verify # does the local map still agree with rig?
|
|
||||||
```
|
|
||||||
|
|
||||||
`rm -rf berth/` is the uninstall.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What berth is
|
|
||||||
|
|
||||||
**One description of an estate, with a swappable executor.** The description is the artifact;
|
|
||||||
the tool that runs it is a rendering.
|
|
||||||
|
|
||||||
| layer | what berth uses | note |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| overlay | **WireGuard**, config generated per peer | GPL-2.0, in-kernel, and **no coordination server** |
|
|
||||||
| infra | **OpenTofu** — one executor, not a pair | plain `.tf`; `terraform` works identically |
|
|
||||||
| gateway | Caddy locally, nginx on the box | a projection with per-target rules, not a format conversion |
|
|
||||||
| pipeline | Woodpecker | Actions/GitLab reachable from the same description; not built |
|
|
||||||
|
|
||||||
The estate's facts — domain, hosts, ports, instance size, firewall rules, services — live in
|
|
||||||
**one file every rendering reads** (`estate/<name>.json`), rather than being restated in
|
|
||||||
one tool's language and again in another's. **Swappability is bought by the description, not
|
|
||||||
by maintaining two renderings** — a rendering you *can* produce, not one you *must* keep in
|
|
||||||
step. Two live renderings cost you every resource twice, forever, with nothing enforcing that
|
|
||||||
they agree.
|
|
||||||
|
|
||||||
**The seam belongs in a script, not in a tool.** A tool's schema is a ceiling you do not
|
|
||||||
control.
|
|
||||||
|
|
||||||
*(This once leaned on a specific precedent, since withdrawn — `✖ B2` in [STALE.md](STALE.md).)*
|
|
||||||
|
|
||||||
**OpenTofu, and only OpenTofu.** Terraform has been BUSL-licensed since 2023; OpenTofu is the
|
|
||||||
CLI-compatible MPL-2.0 fork (Linux Foundation). Write plain `.tf` that runs under both; the
|
|
||||||
scripts say `tofu`, and `terraform` works identically.
|
|
||||||
|
|
||||||
**Pipelines are the second executor axis, and are not built.** Woodpecker is the
|
|
||||||
self-hosted rendering; Actions and GitLab CI are the standards that must be reachable from
|
|
||||||
the same description. Named here so the infra seam is not designed in a way that forecloses
|
|
||||||
it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The overlay is berth's network layer
|
|
||||||
|
|
||||||
Two instances in different clouds cannot share a VPC. A WireGuard overlay gives them one flat
|
|
||||||
address space that berth owns and can reproduce on any provider — and, under a flaky
|
|
||||||
environment, a second layer beneath whatever the provider offers.
|
|
||||||
|
|
||||||
That inverts the usual cloud pattern. Instead of a VPC with security groups and private
|
|
||||||
subnets, each instance gets a public IP, opens **only** the WireGuard port, and carries
|
|
||||||
everything else inside the tunnel. **The security boundary moves out of the provider's VPC
|
|
||||||
and into a layer that is identical on AWS, on GCP, and on a laptop behind NAT.**
|
|
||||||
|
|
||||||
**Plain WireGuard, not Tailscale/Headscale/NetBird.** Tailscale's client is open but its
|
|
||||||
coordination plane is proprietary SaaS; Headscale and NetBird are open but add a control plane
|
|
||||||
to run. Plain WireGuard needs no server at all.
|
|
||||||
|
|
||||||
**The cost is real and accepted:** no NAT traversal, no relay, no peer discovery. A peer behind
|
|
||||||
NAT must dial one with a public endpoint. Fine here — the instances have public IPs and the dev
|
|
||||||
box roams — but two roaming peers cannot reach each other. That is why `PersistentKeepalive` is
|
|
||||||
a checked invariant rather than a detail.
|
|
||||||
|
|
||||||
**Keys never enter the description.** Private keys are generated on the peer that owns them
|
|
||||||
(`make vpn keygen`) into `ctrl/.secrets/` and injected only at render time; public keys live in
|
|
||||||
the estate, because a config cannot be built without them. `vpn.sh` **refuses to write** either
|
|
||||||
a key or a rendered config until `git check-ignore` confirms the path is ignored — this repo
|
|
||||||
has already been bitten once by a `.gitignore` pattern anchoring to the wrong directory.
|
|
||||||
|
|
||||||
This also decides something about the IaC layer: **OpenTofu must never generate a WireGuard
|
|
||||||
private key**, because Terraform-lineage state stores every resource attribute in plaintext.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Two rules that are not style preferences
|
|
||||||
|
|
||||||
### 1. Every default is the read-only verb
|
|
||||||
|
|
||||||
```
|
|
||||||
make estate -> show make certs -> status
|
|
||||||
make dns -> list make host -> status
|
|
||||||
make estate apply / destroy -> print the plan, then refuse without --yes
|
|
||||||
```
|
|
||||||
|
|
||||||
rig's `make cluster` defaults to `up`, because every rig verb is safe — a kind cluster is
|
|
||||||
disposable. berth's are not: `tofu destroy` costs money and takes live DNS with it. **A
|
|
||||||
tool where every verb is safe must not grow verbs that are not.**
|
|
||||||
|
|
||||||
The failure this prevents is not hypothetical. `ppl/ctrl/certs.sh:42` is `CMD="${1:-all}"`,
|
|
||||||
so a bare `./ctrl/certs.sh` there issues a real Let's Encrypt cert, rsyncs it to the gateway,
|
|
||||||
and reloads nginx. berth's `certs` defaults to `status`.
|
|
||||||
|
|
||||||
### 2. berth and rig share a convention, not code
|
|
||||||
|
|
||||||
Neither imports the other. They match on **shape** — the `make <noun> <verb>` dispatch, the
|
|
||||||
four-source config layering, the key names — and consistency is verified by
|
|
||||||
**recomputation**: `make ports verify` recomputes rig's `20000 + (cksum(name) % 200) * 10`
|
|
||||||
to check the local map, rather than sourcing rig's `lib/config.sh`.
|
|
||||||
|
|
||||||
Copying three stable lines is the whole cost of not coupling them. A shared library would
|
|
||||||
put something outside `rig/` on rig's path, and rig's promise is that
|
|
||||||
`grep -rIn -iE 'soleprint|\bspr\b'` across it returns nothing.
|
|
||||||
|
|
||||||
**rig is also unaware that berth exists.** `ppl/local/Caddyfile` is berth's to generate; rig
|
|
||||||
must not reference `local.ar` — its handover scrub refuses the string.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The gateway doctrine
|
|
||||||
|
|
||||||
Practised across this codebase for a long time and never written down, so: written down.
|
|
||||||
|
|
||||||
- **Caddy where routing is dynamic and config-driven** — the in-cluster gateway that
|
|
||||||
multiplexes by Host header, and the host-side `.local.ar` name→port map.
|
|
||||||
- **nginx where it is a static server or a plain long-running compose service on the box.**
|
|
||||||
- **Envoy in `mpr`** — a deliberate one-off, not a third pattern.
|
|
||||||
- **`ingress-nginx` only as a kind addon** — a different thing again from either gateway.
|
|
||||||
|
|
||||||
### Rendering is a projection, not a format conversion
|
|
||||||
|
|
||||||
The local Caddyfile and the box's nginx are not two spellings of the same content:
|
|
||||||
|
|
||||||
| | local (Caddy) | cloud (nginx) |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| granularity | one file | one file per vhost |
|
|
||||||
| blocks per service | one | two (`:80` redirect + `:443` server) |
|
|
||||||
| TLS | none; every address needs an explicit `:80` | one shared wildcard cert |
|
|
||||||
| upstream | `localhost:<port>` | container name + `resolver 127.0.0.11` |
|
|
||||||
| name depth | free | constrained by the cert |
|
|
||||||
| ambiguity | most specific wins | exact, else `default_server` (= load order) |
|
|
||||||
|
|
||||||
The `:80` is not decoration: without it Caddy 2 defaults each site to `:443` with auto-HTTPS,
|
|
||||||
which on `*.local.ar` means cert provisioning that fails and breaks the listener. The
|
|
||||||
variable upstream is not decoration either: naming the upstream in a variable forces runtime
|
|
||||||
DNS resolution, so nginx **starts when the upstream container is absent** — which is what
|
|
||||||
lets one nginx front a dozen independent compose stacks.
|
|
||||||
|
|
||||||
Because the two disambiguate by **opposite** rules, a name set that is unambiguous locally
|
|
||||||
can be ambiguous on the box. `make check` asserts against the projection, not the source.
|
|
||||||
|
|
||||||
### Installing generated vhosts — an order that is not optional
|
|
||||||
|
|
||||||
1. Generated config lands in `conf.d/generated/`, **not** `conf.d/`. `ppl/ctrl/deploy.sh`
|
|
||||||
rsyncs with `--delete`; sharing a directory means one set gets erased.
|
|
||||||
2. `nginx.conf` needs a **third** include line — its `conf.d/*.conf` glob does not recurse,
|
|
||||||
which is why `conf.d/soleprint/*.conf` already needs its own.
|
|
||||||
3. That include changes **load order**, and load order decides which `:443` block catches
|
|
||||||
unmatched names. So `default.conf`'s commented-out `:443 default_server` must be restored
|
|
||||||
**first**. `make check` fails on it deliberately: it is a gate, not a warning.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What the checks assert, and why
|
|
||||||
|
|
||||||
The scripts are deliberately thin on comment — the reasoning lives here. Every check below
|
|
||||||
corresponds to something that is wrong, or was wrong, in a real estate.
|
|
||||||
|
|
||||||
### `make check` — the estate
|
|
||||||
|
|
||||||
| assertion | the failure it catches |
|
|
||||||
| --- | --- |
|
|
||||||
| every service name is covered by an issued cert SAN | **a wildcard matches exactly one label.** `*.d.com` covers `a.d.com` but not `a.b.d.com`, which needs its own SAN. Surfaces otherwise as a browser TLS warning, far from its cause |
|
|
||||||
| a `:443 default_server` exists | DNS and the cert are wildcard but nginx matches `server_name` exactly, so without one the fallback for any unknown name is whichever vhost loads first — alphabetically, by accident |
|
|
||||||
| firewall rules and listeners agree | a rule allowing a port nothing listens on is **dead config**; a service no compose file declares is **undocumented state**. Neither is visible from one side alone, which is why the inventory has two halves |
|
|
||||||
| `HOST` is an ssh alias, never a hostname | there is no `Host <domain>` block, so a bare hostname falls through to the global defaults, ssh offers every key in the agent in turn, and `MaxAuthTries` (6) trips with *"Too many authentication failures"* before reaching the right one. The aliases set `IdentitiesOnly yes` |
|
|
||||||
|
|
||||||
### `make vpn check` — the overlay
|
|
||||||
|
|
||||||
| assertion | the failure it catches |
|
|
||||||
| --- | --- |
|
|
||||||
| peer addresses unique and inside the subnet | a duplicate is a silent misroute, never an error |
|
|
||||||
| AllowedIPs do not overlap | AllowedIPs is **cryptokey routing** — the route table and the ACL at once. Overlapping ranges resolve to the last match, so an overlap is both a misroute and an unintended grant |
|
|
||||||
| something carries `PersistentKeepalive` if anything roams | without it a NAT mapping expires and the tunnel works only while traffic flows outward — *"works sometimes"*, the hardest failure to read. Note it is **not** a property of the roaming peer's own entry: the roaming machine sets it on the entry for the peer it **dials** |
|
|
||||||
| the listen port is in the firewall | otherwise no peer can be dialed at all |
|
|
||||||
| no private key in the description | public keys are *also* 44-char base64, so the shape proves nothing. The real assertions are **no field named `priv*`** and **no key outside a `public_key` field** |
|
|
||||||
| overlay-reached services bind a reachable address | **a tunnel cannot reach loopback.** A service on `127.0.0.1` is unreachable over the overlay; one on `0.0.0.0` is reachable but also exposed to the whole LAN |
|
|
||||||
|
|
||||||
### Capturing the overlay
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo wg show | make vpn capture --write
|
|
||||||
```
|
|
||||||
|
|
||||||
`wg show` has three forms and **only the first is safe**:
|
|
||||||
|
|
||||||
| form | safe | why |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `wg show` | **yes** | prints `private key: (hidden)` |
|
|
||||||
| `wg show <if> dump` | **no** | field 1 of the first line *is* the private key |
|
|
||||||
| `wg showconf <if>` | **no** | prints `PrivateKey=` outright |
|
|
||||||
|
|
||||||
`capture` refuses the latter two by shape. It matches peers by **allowed-ips address, not
|
|
||||||
public key** — the keys are exactly what is missing at that point — and **drops a roaming
|
|
||||||
peer's endpoint in the parser**, since that value is a home ISP address and a roaming peer
|
|
||||||
has no stable endpoint anyway.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
berth/
|
|
||||||
├── Makefile one target per ctrl/ script; the verb is an argument
|
|
||||||
├── STALE.md withdrawn assumptions, each with a check that runs
|
|
||||||
├── estate/<name>.json THE ARTIFACT — one description, many renderings
|
|
||||||
└── ctrl/
|
|
||||||
├── check.sh reports and instructs; never fixes
|
|
||||||
├── estate.sh show | list | plan | apply --yes | destroy --yes
|
|
||||||
├── services.sh list | render <aws|gcp|local> | deploy
|
|
||||||
├── ports.sh show | verify (the rig coincidence check)
|
|
||||||
├── dns.sh certs.sh host.sh registry.sh docs.sh
|
|
||||||
├── versions.env pinned toolchain (weakest layer)
|
|
||||||
├── env.d/<target>.env provider shape: aws | gcp
|
|
||||||
├── .env.example -> ctrl/.env, machine-local (gitignored)
|
|
||||||
├── lib/config.sh the four-layer load, from rig
|
|
||||||
├── lib/estate.sh reading and projecting the estate
|
|
||||||
└── render/*.tmpl nginx vhost shapes, substituted with sed
|
|
||||||
```
|
|
||||||
|
|
||||||
Config layers, weakest first: `versions.env` → `env.d/<target>.env` → `ctrl/.env` → the
|
|
||||||
caller's environment. So `make estate plan TARGET=gcp` beats everything.
|
|
||||||
|
|
||||||
**Identity is explicit — the inversion of rig.** rig derives its name from its folder so that
|
|
||||||
copies never collide. berth refuses to guess, because a deployment has exactly one production
|
|
||||||
and a wrong guess acts on the wrong estate. `ESTATE` names a file; the only convenience is
|
|
||||||
that a single `estate/*.json` is used without being asked for.
|
|
||||||
|
|
||||||
**`python3`, not `jq`.** rig ships a pinned static `jq` because its floor is "docker and
|
|
||||||
nothing else" on a machine it does not control. berth's floor is already higher, so
|
|
||||||
`python3` is a dependency it *has* rather than one it *adds* — the same reasoning by which
|
|
||||||
rig chose `sed` over `envsubst`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
**B0 (this) is the shape.** `estate/mcrn.json` is marked `UNVERIFIED`: it records what the
|
|
||||||
repos *claim*, because `ppl/infra/` was written and never applied — no `~/.pulumi`, no
|
|
||||||
`venv`, no stack state, files dated `mar 6`. B1's read-only inventory is what replaces those
|
|
||||||
claims with observations. Until then, `estate plan` and `estate apply` refuse: there is
|
|
||||||
nothing truthful to compare against yet.
|
|
||||||
|
|
||||||
`make check` currently fails on two real things — see `def/plans/36.0/berth.md`.
|
|
||||||
104
berth/STALE.md
104
berth/STALE.md
@@ -1,104 +0,0 @@
|
|||||||
# berth — withdrawn assumptions
|
|
||||||
|
|
||||||
**Everything in this file is no longer true.**
|
|
||||||
|
|
||||||
It exists so the live docs stay short and so a withdrawn assumption cannot quietly return:
|
|
||||||
each entry carries a **check**, and `ctrl/selftest.sh` runs every one of them. A retraction
|
|
||||||
that is only prose is a retraction nobody re-reads.
|
|
||||||
|
|
||||||
Kept rather than deleted for the reason the docgen thread already wrote down —
|
|
||||||
*a requirement that disappears without explanation comes back.*
|
|
||||||
|
|
||||||
### For agents
|
|
||||||
|
|
||||||
- **Do not restate these** in a plan, a README or a comment. One line pointing here is enough.
|
|
||||||
- **Ids are stable.** Cite `✖ B3`; do not re-explain it.
|
|
||||||
- **When you withdraw an assumption, move it here** — quoted claim, where it came from, what
|
|
||||||
superseded it and why, what changed in the code, and a check that proves it is gone.
|
|
||||||
- **Only withdrawn things belong here.** A warning that is still actionable is a live rule,
|
|
||||||
however historical it sounds, and stays where it is.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**✖ B1 — "Pulumi is the source in spr, and Terraform must match the config."**
|
|
||||||
*(INDEX §5, carried from 35.3)* Withdrawn 2026-09-12. berth uses **OpenTofu and only
|
|
||||||
OpenTofu**. The rule assumed *open source* and *industry standard* pull apart — Terraform
|
|
||||||
being BUSL, Pulumi being the open alternative. OpenTofu is both: the standard language, plain
|
|
||||||
Terraform-compatible HCL, under MPL-2.0. Swappability was never bought by keeping two
|
|
||||||
renderings; it is bought by `estate/*.json` being the artifact — a rendering you *can*
|
|
||||||
produce, not one you *must* maintain.
|
|
||||||
**Gone from:** `ctrl/versions.env` (no `PULUMI_VERSION`), `ctrl/estate.sh` (`plan` runs one
|
|
||||||
executor), `ctrl/lib/config.sh` (`PULUMI_STACK` → `TOFU_WORKSPACE`), `ctrl/check.sh`
|
|
||||||
(toolchain list).
|
|
||||||
**Deliberately kept:** `README.md` and `estate/mcrn.json` both record that `ppl/infra/` was
|
|
||||||
written and never applied — *"no `~/.pulumi`, no venv, no stack state"*. That is a historical
|
|
||||||
fact about the estate, not a live dependency.
|
|
||||||
**Check:** no `pulumi` in `ctrl/` or `Makefile`.
|
|
||||||
|
|
||||||
**✖ B2 — "ctlptl's rejection is the precedent for *the seam belongs in a script, not a tool*."**
|
|
||||||
*(INDEX §5, `berth/README.md`)* Withdrawn 2026-09-12. **ctlptl was reinstated** — pinned in
|
|
||||||
`rig/ctrl/versions.env` at v0.9.4 — and had been removed for the wrong reason. The rule may
|
|
||||||
still hold; it now has to stand on its own reasoning rather than that example.
|
|
||||||
**Gone from:** `README.md` — the argument is stated directly, with no borrowed evidence.
|
|
||||||
**Check:** `ctlptl` appears nowhere in berth.
|
|
||||||
|
|
||||||
**✖ B3 — "`wg show` is safe; `wg showconf` is not."** *(my own note, 2026-09-12)* Incomplete,
|
|
||||||
and the gap is the dangerous one. There are **three** forms, and `wg show <if> dump` puts the
|
|
||||||
**private key in field 1 of the first line**. Stated as a two-way distinction, the `dump` form
|
|
||||||
reads as safe.
|
|
||||||
**Now:** `wg show` plain is safe; `dump` and `showconf` are not. `ctrl/vpn.sh capture` refuses
|
|
||||||
the latter two **by shape**, rather than parsing around them.
|
|
||||||
**Check:** `vpn.sh` names all three forms, and `capture` rejects both unsafe ones.
|
|
||||||
|
|
||||||
**✖ B4 — "A roaming peer must set `PersistentKeepalive` on its own entry."**
|
|
||||||
*(`ctrl/vpn.sh`, first draft)* Wrong side, and wrong in the direction that looks fine:
|
|
||||||
`PersistentKeepalive` is set per-peer in a config, so the roaming machine sets it on the entry
|
|
||||||
for the peer it **dials**. The original check would have **warned on a correctly configured
|
|
||||||
overlay**.
|
|
||||||
**Now:** checked once per overlay — if anything roams, some peer entry must carry a keepalive.
|
|
||||||
**Check:** the invariant is not keyed on the roaming peer's own `keepalive` field.
|
|
||||||
|
|
||||||
**✖ B5 — "The Makefile's pass-through block goes near the top, with the other variables."**
|
|
||||||
*(`Makefile`, inherited from rig's layout)* Withdrawn 2026-09-12. When a subcommand **names a
|
|
||||||
real target**, make has two recipes for it and the **last definition wins** — so with the block
|
|
||||||
first, `make host ports` ran `ctrl/host.sh ports` *and* `ctrl/ports.sh ports`, the second
|
|
||||||
failing because `ports` is not one of its verbs. Same for `make host services`, `make vpn
|
|
||||||
check`, `make vpn show estate`.
|
|
||||||
**Now:** the `$(eval $(ARGS):;@:)` block is **last in the file**, so the no-op wins and the
|
|
||||||
word is swallowed — which is what an argument is. Make's *"overriding recipe"* warning is the
|
|
||||||
swallow working.
|
|
||||||
**Check:** every colliding invocation dispatches to exactly one script.
|
|
||||||
|
|
||||||
**✖ B6 — "A base64 key in the description can be caught by its shape."**
|
|
||||||
*(`ctrl/vpn.sh check`, first draft)* WireGuard **public** keys are also 44-char base64 and
|
|
||||||
legitimately live in the estate, so shape alone proves nothing and would flag correct data.
|
|
||||||
**Now:** two assertions instead — **no field named `priv*`**, and **no base64 key outside a
|
|
||||||
`public_key` field**.
|
|
||||||
**Check:** the estate's public keys do not trip the secret check.
|
|
||||||
|
|
||||||
**✖ B7 — "`network.wireguard` is where the overlay is described."** *(`estate/mcrn.json`)*
|
|
||||||
Superseded 2026-09-12: WireGuard is berth's network layer, not one service's transport, so it
|
|
||||||
is a top-level `vpn` block with named overlays and peers. `ctrl/check.sh` and
|
|
||||||
`ctrl/registry.sh` were repointed.
|
|
||||||
**Gone from:** the estate schema — a `wireguard_moved` tombstone marks the old key.
|
|
||||||
**Check:** nothing reads `network.wireguard.*`.
|
|
||||||
|
|
||||||
**✖ B8 — "berth and rig are related through their first uses."** *(early framing)* Withdrawn:
|
|
||||||
**rig and berth are peers — neither depends on the other.** They match on shape (dispatch,
|
|
||||||
config layering, key names) and consistency is verified by **recomputation**, never by
|
|
||||||
dependency. A shared library would put something outside `rig/` on rig's path.
|
|
||||||
**Check:** berth imports nothing from rig; `ports.sh` recomputes the port formula and agrees
|
|
||||||
with rig's golden values.
|
|
||||||
|
|
||||||
**✖ B9 — "`langfuse.mcrn.ar` is an exception that cannot be generated."**
|
|
||||||
*(`estate/mcrn.json`, `raw: true`)* Withdrawn 2026-09-14. It was filed as the one route a
|
|
||||||
template could not express — a static `upstream{}` to a WireGuard address, with no `resolver`
|
|
||||||
and no `set $var`. It was not an exception; it was **the first instance of the general case**.
|
|
||||||
Those three properties are not three decisions, they are one: *this service is reached by
|
|
||||||
address on the overlay, not by name on the docker network.* Naming that decision —
|
|
||||||
`placement` — makes the file renderable.
|
|
||||||
**Gone from:** `estate/mcrn.json` — `lng` and `langfuse` were **two entries for one socket**
|
|
||||||
and are now one service with `placement: local`, `peer: nrft`, and a `local_host` for the
|
|
||||||
name it answers to locally. `raw` is dropped.
|
|
||||||
**Check:** the generated vhost matches the hand-written one, normalised for comments and
|
|
||||||
whitespace — proof against a live route rather than an assertion.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Machine-local config. Copy to ctrl/.env (gitignored) and edit.
|
|
||||||
#
|
|
||||||
# The estate's FACTS live in estate/<name>.json.
|
|
||||||
# The provider's SHAPE lives in ctrl/env.d/<target>.env.
|
|
||||||
# This file is only what differs between machines, plus credentials.
|
|
||||||
|
|
||||||
# ESTATE is required when estate/ holds more than one file.
|
|
||||||
# ESTATE=mcrn
|
|
||||||
# TARGET=aws
|
|
||||||
|
|
||||||
# ssh ALIASES, never hostnames — check.sh refuses a value containing a dot.
|
|
||||||
# HOST=mcrn # app user, no sudo
|
|
||||||
# HOST_ADMIN=mcrn-admin # sudo, only where genuinely required
|
|
||||||
|
|
||||||
# Credentials: names only, never values. The secrets stay in ~/.aws and
|
|
||||||
# ~/.config/gcloud where their own tooling manages them.
|
|
||||||
# AWS_PROFILE=default
|
|
||||||
# GCP_PROJECT=
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# The wildcard TLS cert for the gateway.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./certs.sh status # SANs issued vs SANs the services need
|
|
||||||
# ./certs.sh verify # inspect the cert served on :443
|
|
||||||
# ./certs.sh renew # refuses: issues a real cert
|
|
||||||
# ./certs.sh push # refuses: ships to a live gateway
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
status() {
|
|
||||||
local issued; issued=$(estate_get "certs.issued" | python3 -c 'import json,sys
|
|
||||||
try: print("\n".join(json.load(sys.stdin)))
|
|
||||||
except Exception: pass')
|
|
||||||
echo "issued SANs (estate/${ESTATE}.json: certs.issued):"
|
|
||||||
echo "$issued" | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
echo "SANs the services NEED (derived from services[]):"
|
|
||||||
estate_sans | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
local missing=0 s
|
|
||||||
while IFS= read -r s; do
|
|
||||||
[ -z "$s" ] && continue
|
|
||||||
grep -qxF "$s" <<< "$issued" || { echo "MISSING: $s"; missing=1; }
|
|
||||||
done < <(estate_sans)
|
|
||||||
[ "$missing" = 0 ] && echo "the issued cert covers every derived name."
|
|
||||||
echo
|
|
||||||
echo "certbot image: $(eval echo "\$$CERTBOT_IMAGE_VAR") provider: $DNS_PROVIDER"
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
|
||||||
echo "would run:"
|
|
||||||
echo " echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null \\"
|
|
||||||
echo " | openssl x509 -noout -dates -ext subjectAltName"
|
|
||||||
echo
|
|
||||||
echo "read-only against a live host — announce and approve first (§7)."
|
|
||||||
}
|
|
||||||
|
|
||||||
refuse() {
|
|
||||||
echo "REFUSING: '$1' acts on a live cert and a live gateway." >&2
|
|
||||||
echo " renew issues a real Let's Encrypt cert (rate-limited)." >&2
|
|
||||||
echo " push rsyncs to the gateway and reloads nginx." >&2
|
|
||||||
echo " Neither runs without explicit approval." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-status}" in
|
|
||||||
status) status ;;
|
|
||||||
verify) verify ;;
|
|
||||||
renew|push) refuse "$1" ;;
|
|
||||||
*) echo "usage: $0 [status|verify|renew|push]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Is this estate coherent? Reports and instructs; never fixes.
|
|
||||||
#
|
|
||||||
# Takes no subcommand — there is one question to ask.
|
|
||||||
# Every check corresponds to something wrong in the estate today.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
WORST=0
|
|
||||||
note() { echo " $*"; }
|
|
||||||
warn() { echo " WARN $*"; [ "$WORST" -lt 1 ] && WORST=1; return 0; }
|
|
||||||
bad() { echo " FAIL $*"; WORST=2; return 0; }
|
|
||||||
|
|
||||||
echo "estate: $ESTATE target: $TARGET domain: $DOMAIN"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# 1. cert coverage: a wildcard matches exactly one label.
|
|
||||||
echo "certs — does the cert cover every name the services serve?"
|
|
||||||
issued=$(estate_get "certs.issued" | python3 -c 'import json,sys
|
|
||||||
try: print("\n".join(json.load(sys.stdin)))
|
|
||||||
except Exception: pass')
|
|
||||||
if [ -z "$issued" ]; then
|
|
||||||
warn "no certs.issued in the estate file — cannot check coverage."
|
|
||||||
else
|
|
||||||
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
||||||
[ -z "$host" ] && continue
|
|
||||||
fqdn="${host}.${DOMAIN}"
|
|
||||||
# A '*' host stands for "any single label here" — check the deepest
|
|
||||||
# name it can produce, which is the one that fails.
|
|
||||||
probe="$fqdn"
|
|
||||||
case "$host" in \*.*) probe="anyroom.${host#\*.}.${DOMAIN}" ;; esac
|
|
||||||
covered=""
|
|
||||||
while IFS= read -r san; do
|
|
||||||
[ -z "$san" ] && continue
|
|
||||||
if san_covers "$probe" "$san"; then covered=1; break; fi
|
|
||||||
done <<< "$issued"
|
|
||||||
if [ -z "$covered" ]; then
|
|
||||||
bad "$name: '$probe' is covered by NO issued SAN"
|
|
||||||
note " issued: $(echo "$issued" | tr '\n' ' ')"
|
|
||||||
note " a wildcard matches exactly ONE label — reissue with"
|
|
||||||
note " -d '*.${host#\*.}.${DOMAIN}' or move the name one level up"
|
|
||||||
fi
|
|
||||||
done < <(estate_services "$TARGET")
|
|
||||||
[ "$WORST" -lt 2 ] && note "every service name is covered."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# 2. unmatched names: DNS and the cert are wildcard, nginx is exact, so
|
|
||||||
# without a :443 default_server the fallback is whichever vhost loads first.
|
|
||||||
echo "gateway — is there a deliberate answer for unmatched names?"
|
|
||||||
DEFAULT_CONF="${PPL_DIR:-$HOME/wdir/semester/ppl}/gateway/nginx/conf.d/default.conf"
|
|
||||||
if [ ! -f "$DEFAULT_CONF" ]; then
|
|
||||||
note "ppl not on this machine at $DEFAULT_CONF — skipped."
|
|
||||||
elif grep -qE '^\s*listen\s+443.*default_server' "$DEFAULT_CONF"; then
|
|
||||||
note "default.conf has a :443 default_server."
|
|
||||||
else
|
|
||||||
bad "default.conf has NO :443 default_server."
|
|
||||||
note " Unmatched names fall through to the first-loaded vhost."
|
|
||||||
note " This is a PREREQUISITE for generating any config: adding a"
|
|
||||||
note " generated include changes load order, and load order is what"
|
|
||||||
note " currently decides the fallback."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# 3. a rule allowing a port nothing listens on is dead config; a service no
|
|
||||||
# compose file declares is undocumented state. Needs both halves to see.
|
|
||||||
echo "firewall — rules against listeners"
|
|
||||||
estate_get "firewall" | python3 -c '
|
|
||||||
import json,sys
|
|
||||||
try: fw = json.load(sys.stdin)
|
|
||||||
except Exception: fw = []
|
|
||||||
for r in fw:
|
|
||||||
n = r.get("note")
|
|
||||||
print(" %-6s %-5s %s" % (r["port"], r.get("proto","tcp"), r.get("desc","")))
|
|
||||||
if n: print(" UNRESOLVED: " + n)
|
|
||||||
'
|
|
||||||
note "listener side: unknown until captured (ss -ltnp over ssh $HOST)."
|
|
||||||
# Ask the structure, not the prose: the condition is "does a peer still lack a
|
|
||||||
# public key", not "is there a _status string". _status is ALWAYS non-empty —
|
|
||||||
# capture rewrites it to "CAPTURED ..." — so testing it for emptiness pinned
|
|
||||||
# this warning on permanently, including after the capture it asks for.
|
|
||||||
uncaptured=$(estate_get "vpn.overlays.estate.peers" 2>/dev/null | python3 -c '
|
|
||||||
import json, sys
|
|
||||||
try:
|
|
||||||
peers = json.load(sys.stdin)
|
|
||||||
except Exception:
|
|
||||||
sys.exit(0)
|
|
||||||
print(" ".join(n for n, p in peers.items() if not p.get("public_key")))
|
|
||||||
' 2>/dev/null)
|
|
||||||
if [ -n "$uncaptured" ]; then
|
|
||||||
warn "overlay: public keys not captured for:$uncaptured — see 'make vpn check'"
|
|
||||||
note " 10.8.0.1 carries the registry and woodpecker gRPC;"
|
|
||||||
note " 10.8.0.2 backs langfuse. Nothing in the tree creates the"
|
|
||||||
note " interface — a fresh box cannot start the gateway compose."
|
|
||||||
note " capture with: sudo wg show | make vpn capture --write"
|
|
||||||
else
|
|
||||||
note "overlay: $(estate_get 'vpn._status')"
|
|
||||||
fi
|
|
||||||
note "overlay detail: make vpn show estate"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# 4. ssh aliases, never hostnames: a bare hostname offers every agent key and
|
|
||||||
# trips MaxAuthTries before reaching the right one.
|
|
||||||
echo "ssh — aliases, never hostnames"
|
|
||||||
for var in HOST HOST_ADMIN; do
|
|
||||||
val="${!var:-}"
|
|
||||||
if [ -z "$val" ]; then
|
|
||||||
warn "$var is unset."
|
|
||||||
elif [[ "$val" == *.* ]]; then
|
|
||||||
bad "$var='$val' looks like a hostname, not a ~/.ssh/config alias."
|
|
||||||
note " A bare hostname trips MaxAuthTries before reaching the key."
|
|
||||||
elif [ -f "$HOME/.ssh/config" ] && grep -qiE "^\s*Host\s+.*\b${val}\b" "$HOME/.ssh/config"; then
|
|
||||||
note "$var=$val — Host block present."
|
|
||||||
else
|
|
||||||
warn "$var='$val' has no matching Host block in ~/.ssh/config."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
for f in "$HOME/wdir/semester/ppl/ctrl/.env"; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
if grep -qE '^SERVER=.*\.' "$f"; then
|
|
||||||
warn "$f sets SERVER to a hostname, not an alias — every ppl script inherits it."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ── 5. toolchain ───────────────────────────────────────────────────────────
|
|
||||||
echo "toolchain"
|
|
||||||
for t in python3 "$TOFU_BIN" aws gcloud ssh rsync wg; do
|
|
||||||
if command -v "$t" >/dev/null 2>&1; then
|
|
||||||
note "$(printf '%-8s' "$t") present"
|
|
||||||
else
|
|
||||||
note "$(printf '%-8s' "$t") MISSING — $(case $t in
|
|
||||||
tofu) echo 'blocks: estate plan/apply; terraform works identically' ;;
|
|
||||||
wg) echo 'blocks: vpn keygen and the overlay checks' ;;
|
|
||||||
aws) echo 'blocks: the control-plane inventory, dns on route53' ;;
|
|
||||||
gcloud) echo 'blocks: the gcp estate' ;;
|
|
||||||
*) echo 'blocks: most things' ;;
|
|
||||||
esac)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
|
|
||||||
case "$WORST" in
|
|
||||||
0) echo "OK" ;;
|
|
||||||
1) echo "OK, with warnings" ;;
|
|
||||||
2) echo "PROBLEMS FOUND — see FAIL lines above" ;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# DNS records, over whichever provider the target names.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./dns.sh list
|
|
||||||
# ./dns.sh add <subdomain> # <sub>.<domain> -> <domain>
|
|
||||||
# ./dns.sh add-wildcard <sub>
|
|
||||||
# ./dns.sh remove <subdomain>
|
|
||||||
#
|
|
||||||
# Read-only verbs announce and wait; mutating ones refuse.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
announce() {
|
|
||||||
echo "would run:"
|
|
||||||
printf ' %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
list() {
|
|
||||||
case "$DNS_PROVIDER" in
|
|
||||||
route53)
|
|
||||||
announce "aws route53 list-resource-record-sets --hosted-zone-id $AWS_HOSTED_ZONE_ID" \
|
|
||||||
"--query 'ResourceRecordSets[].[Name,Type,TTL,ResourceRecords[0].Value]' --output table"
|
|
||||||
;;
|
|
||||||
google)
|
|
||||||
announce "gcloud dns record-sets list --zone=${ESTATE}-zone --project=${GCP_PROJECT:-<unset>}"
|
|
||||||
;;
|
|
||||||
*) echo "unknown DNS_PROVIDER: $DNS_PROVIDER" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
echo
|
|
||||||
echo "read-only, but NOT run: each batch is announced and"
|
|
||||||
echo "waited on. Approve it and it runs."
|
|
||||||
}
|
|
||||||
|
|
||||||
mutate() {
|
|
||||||
local verb="$1" sub="${2:-}"
|
|
||||||
[ -z "$sub" ] && { echo "usage: $0 $verb <subdomain>" >&2; exit 1; }
|
|
||||||
local name
|
|
||||||
case "$verb" in
|
|
||||||
add) name="${sub}.${DOMAIN}" ;;
|
|
||||||
add-wildcard) name="*.${sub}.${DOMAIN}" ;;
|
|
||||||
remove) name="${sub}.${DOMAIN}" ;;
|
|
||||||
esac
|
|
||||||
echo "$verb: $name -> $DOMAIN (provider: $DNS_PROVIDER)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# The wildcard-depth rule again, applied BEFORE the record is created rather
|
|
||||||
# than discovered in a browser afterwards.
|
|
||||||
if [ "$verb" = "add" ]; then
|
|
||||||
local issued; issued=$(estate_get "certs.issued" | python3 -c 'import json,sys
|
|
||||||
try: print("\n".join(json.load(sys.stdin)))
|
|
||||||
except Exception: pass')
|
|
||||||
local covered=""
|
|
||||||
while IFS= read -r san; do
|
|
||||||
[ -z "$san" ] && continue
|
|
||||||
san_covers "$name" "$san" && { covered=1; break; }
|
|
||||||
done <<< "$issued"
|
|
||||||
[ -z "$covered" ] && {
|
|
||||||
echo "WARNING: '$name' is covered by no issued SAN." >&2
|
|
||||||
echo " A wildcard matches ONE label. The record would" >&2
|
|
||||||
echo " resolve and then fail TLS. Reissue the cert first." >&2
|
|
||||||
echo >&2
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "REFUSING: this changes live DNS." >&2
|
|
||||||
echo " Nothing is created, modified or deleted on any account without" >&2
|
|
||||||
echo " explicit approval for that specific action." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-list}" in
|
|
||||||
list) list ;;
|
|
||||||
add|add-wildcard|remove) mutate "$@" ;;
|
|
||||||
*) echo "usage: $0 [list|add <sub>|add-wildcard <sub>|remove <sub>]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Documentation.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./docs.sh serve|graphs
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
case "${1:-serve}" in
|
|
||||||
serve)
|
|
||||||
echo "berth has no doc server yet — the README is the documentation."
|
|
||||||
echo " $(cd .. && pwd)/README.md"
|
|
||||||
echo
|
|
||||||
echo "the estate, resolved: make estate show"
|
|
||||||
;;
|
|
||||||
graphs)
|
|
||||||
echo "not implemented. The estate file is the graph's source; a"
|
|
||||||
echo "renderer belongs with docgen/graphgen, which is another thread's"
|
|
||||||
echo "another thread's — so this is a handoff, not a stub to fill in here."
|
|
||||||
;;
|
|
||||||
*) echo "usage: $0 [serve|graphs]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Target: AWS — the estate that actually runs today (mcrn.ar).
|
|
||||||
TARGET_NAME=aws
|
|
||||||
CLOUD=aws
|
|
||||||
REGION=us-east-1
|
|
||||||
INSTANCE_TYPE=t3.small
|
|
||||||
|
|
||||||
# The Route53 hosted zone. It ALREADY EXISTS and is reused, never created —
|
|
||||||
# see estate.sh's refusal to create a zone on this target.
|
|
||||||
AWS_HOSTED_ZONE_ID=Z02279903503ZMIB5FC1N
|
|
||||||
SSH_KEY_NAME=mcrn
|
|
||||||
|
|
||||||
# certbot's DNS-01 plugin for this provider.
|
|
||||||
CERTBOT_IMAGE_VAR=CERTBOT_AWS_IMAGE
|
|
||||||
DNS_PROVIDER=route53
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Target: GCP — the replica, on its own domain.
|
|
||||||
#
|
|
||||||
# The domain differs from AWS's on purpose: this target CREATES a DNS zone
|
|
||||||
# where aws reuses one, so a shared domain would create a second authoritative
|
|
||||||
# zone and break live DNS. The domain lives in estate/nrft.json, not here.
|
|
||||||
TARGET_NAME=gcp
|
|
||||||
CLOUD=gcp
|
|
||||||
REGION=us-central1
|
|
||||||
INSTANCE_TYPE=e2-small
|
|
||||||
|
|
||||||
GCP_PROJECT=
|
|
||||||
GCP_ZONE=us-central1-a
|
|
||||||
|
|
||||||
# Delegation order: create the zone and let it answer first, then set the
|
|
||||||
# nameservers at the registrar — it validates that they respond.
|
|
||||||
CERTBOT_IMAGE_VAR=CERTBOT_GCP_IMAGE
|
|
||||||
DNS_PROVIDER=google
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# The estate: what it is, what would change, and — behind a gate — changing it.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./estate.sh # show
|
|
||||||
# ./estate.sh list # every estate/*.json
|
|
||||||
# ./estate.sh plan # tofu plan, read-only
|
|
||||||
# ./estate.sh apply --yes # refuses without --yes
|
|
||||||
# ./estate.sh destroy --yes
|
|
||||||
#
|
|
||||||
# Why the default is read-only: ../README.md
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
show() {
|
|
||||||
echo "estate: $ESTATE ($ESTATE_FILE)"
|
|
||||||
echo "target: $TARGET (cloud=$CLOUD region=$REGION)"
|
|
||||||
echo "domain: $DOMAIN"
|
|
||||||
echo "host: $HOST (sudo: $HOST_ADMIN)"
|
|
||||||
echo "workspace: $TOFU_WORKSPACE"
|
|
||||||
echo
|
|
||||||
local status; status=$(estate_get "_meta.status")
|
|
||||||
[ -n "$status" ] && echo " !! $status" && echo
|
|
||||||
|
|
||||||
echo "services in scope for '$TARGET':"
|
|
||||||
local name host up kind raw
|
|
||||||
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
printf ' %-12s %-14s %-24s %s%s\n' \
|
|
||||||
"$name" "${host:--}" "${up:--}" "$kind" \
|
|
||||||
"$([ -n "$raw" ] && echo ' [hand-written]')"
|
|
||||||
done < <(estate_services "$TARGET")
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "cert SANs (derived, not listed):"
|
|
||||||
estate_sans | sed 's/^/ /'
|
|
||||||
}
|
|
||||||
|
|
||||||
list() {
|
|
||||||
local f n
|
|
||||||
printf '%-12s %-16s %s\n' ESTATE DOMAIN STATUS
|
|
||||||
for f in ../estate/*.json; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
n=$(basename "$f" .json)
|
|
||||||
printf '%-12s %-16s %s%s\n' "$n" \
|
|
||||||
"$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("domain",""))' "$f")" \
|
|
||||||
"$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("_meta",{}).get("status",""))' "$f")" \
|
|
||||||
"$([ "$n" = "$ESTATE" ] && echo ' <- this one')"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read-only. Meaningful only once state is imported: against empty state,
|
|
||||||
# plan reports "create N resources", which is not drift.
|
|
||||||
plan() {
|
|
||||||
echo "== $TOFU_BIN plan =="
|
|
||||||
if ! command -v "$TOFU_BIN" >/dev/null; then
|
|
||||||
echo " $TOFU_BIN not installed — skipped." >&2
|
|
||||||
echo " OpenTofu is the MPL-2.0 fork; 'terraform' works identically." >&2
|
|
||||||
else
|
|
||||||
echo " would run: $TOFU_BIN plan -var-file=<(estate)"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
echo "NOTE: not wired up yet, and that is the point. tofu plan against empty"
|
|
||||||
echo " state reports \"create N resources\" — which is not drift, it is an"
|
|
||||||
echo " empty state. It becomes the check that proves the description"
|
|
||||||
echo " matches reality only once state is IMPORTED from the inventory."
|
|
||||||
echo " ppl/infra/ describes an aspiration: it was never applied."
|
|
||||||
}
|
|
||||||
|
|
||||||
# The gate. Two things have to be true: --yes present, AND the plan shown first.
|
|
||||||
require_yes() {
|
|
||||||
local verb="$1"; shift
|
|
||||||
local yes=""
|
|
||||||
for a in "$@"; do [ "$a" = "--yes" ] && yes=1; done
|
|
||||||
if [ -z "$yes" ]; then
|
|
||||||
echo "refusing to $verb without --yes." >&2
|
|
||||||
echo >&2
|
|
||||||
echo " $verb changes a live, billable estate and can take DNS with it." >&2
|
|
||||||
echo " Read the plan first: make estate plan" >&2
|
|
||||||
echo " Then: ./ctrl/estate.sh $verb --yes" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "refusing to $verb: not implemented, and deliberately so." >&2
|
|
||||||
echo " The executor is not wired up, and nothing is imported yet, so" >&2
|
|
||||||
echo " there is nothing truthful to apply." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-show}" in
|
|
||||||
show) show ;;
|
|
||||||
list) list ;;
|
|
||||||
plan) plan ;;
|
|
||||||
apply) shift; require_yes apply "$@" ;;
|
|
||||||
destroy) shift; require_yes destroy "$@" ;;
|
|
||||||
*) echo "usage: $0 [show|list|plan|apply --yes|destroy --yes]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# The remote box — the other half of the inventory.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./host.sh status|ports|services
|
|
||||||
#
|
|
||||||
# Announces what it would run over `ssh $HOST` and does not run it. Always the
|
|
||||||
# ssh alias, never a hostname: there is no `Host mcrn.ar` block, so a bare
|
|
||||||
# hostname offers every agent key and trips MaxAuthTries.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
guard_alias() {
|
|
||||||
if [[ "$HOST" == *.* ]]; then
|
|
||||||
echo "HOST='$HOST' is a hostname, not a ~/.ssh/config alias. Refusing." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
announce_batch() {
|
|
||||||
echo "would run, over 'ssh $HOST':"
|
|
||||||
printf ' %s\n' "$@"
|
|
||||||
echo
|
|
||||||
echo "read-only, and NOT run. Announce-first applies to EACH batch, not"
|
|
||||||
echo "once per session — so the commands can be read and"
|
|
||||||
echo "learned rather than scrolled past."
|
|
||||||
}
|
|
||||||
|
|
||||||
guard_alias
|
|
||||||
case "${1:-status}" in
|
|
||||||
status)
|
|
||||||
announce_batch \
|
|
||||||
"uname -a; uptime; df -h /" \
|
|
||||||
"docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'" \
|
|
||||||
"docker network inspect gateway --format '{{range .Containers}}{{.Name}} {{end}}'" \
|
|
||||||
"systemctl list-units --type=service --state=running --no-pager" \
|
|
||||||
"systemctl list-timers --no-pager"
|
|
||||||
echo
|
|
||||||
echo "sudo-only, over 'ssh $HOST_ADMIN' and only where genuinely needed:"
|
|
||||||
echo " wg show # the WireGuard peers that exist nowhere in the tree"
|
|
||||||
;;
|
|
||||||
ports)
|
|
||||||
announce_batch "ss -ltnp"
|
|
||||||
echo "the estate declares these firewall rules:"
|
|
||||||
estate_get "firewall" | python3 -c '
|
|
||||||
import json,sys
|
|
||||||
for r in json.load(sys.stdin):
|
|
||||||
print(" %-6s %-5s %s" % (r["port"], r.get("proto","tcp"), r.get("desc","")))'
|
|
||||||
;;
|
|
||||||
services)
|
|
||||||
announce_batch "docker compose -f ~/ppl/gateway/docker-compose.yml ps"
|
|
||||||
echo "the estate declares $(estate_services "$TARGET" | grep -c . ) service(s) for target '$TARGET'."
|
|
||||||
echo "the gateway compose declares 8. The difference is sibling repos'"
|
|
||||||
echo "stacks joining the shared 'gateway' network — intended design, but"
|
|
||||||
echo "nothing in the tree lists it. The inventory produces that list."
|
|
||||||
;;
|
|
||||||
*) echo "usage: $0 [status|ports|services]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# Shared config loading. Sourced, never executed. Run from ctrl/.
|
|
||||||
#
|
|
||||||
# Precedence, weakest first:
|
|
||||||
# ctrl/versions.env pinned toolchain (committed)
|
|
||||||
# ctrl/env.d/<target>.env provider shape: aws|gcp (committed)
|
|
||||||
# ctrl/.env machine-local + secrets (gitignored)
|
|
||||||
# the caller's env `make estate plan TARGET=gcp` (always wins)
|
|
||||||
|
|
||||||
CONFIG_OVERRIDABLE="TARGET ESTATE CLOUD REGION INSTANCE_TYPE
|
|
||||||
HOST HOST_ADMIN AWS_PROFILE AWS_HOSTED_ZONE_ID
|
|
||||||
GCP_PROJECT GCP_ZONE TOFU_WORKSPACE"
|
|
||||||
|
|
||||||
# rig's port formula, reproduced rather than imported. cksum because it is
|
|
||||||
# POSIX and gives the same value on every machine. Used to verify, not allocate.
|
|
||||||
derive_port_base() {
|
|
||||||
local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}')
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
_config_restore() {
|
|
||||||
local line
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line"
|
|
||||||
fi
|
|
||||||
done <<< "$1"
|
|
||||||
# A while loop returns its last body command's status; the trailing empty
|
|
||||||
# line would otherwise make this return 1 and trip `set -e` in the caller.
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
# ${!k+x} distinguishes "set but empty" from "unset" — an explicit
|
|
||||||
# FOO= on the command line is a real choice and must survive.
|
|
||||||
if [ -n "${!k+x}" ]; then
|
|
||||||
saved+="$k=$(printf '%q' "${!k}")"$'\n'
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source ./versions.env
|
|
||||||
[ -f ./.env ] && source ./.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
# Re-apply overrides now so TARGET is the caller's before we pick the file.
|
|
||||||
_config_restore "$saved"
|
|
||||||
|
|
||||||
local target="${TARGET:-aws}"
|
|
||||||
if [ ! -f "./env.d/${target}.env" ]; then
|
|
||||||
echo "no such target: env.d/${target}.env" >&2
|
|
||||||
echo "available: $(ls env.d/*.env 2>/dev/null | xargs -n1 basename | sed 's/\.env$//' | tr '\n' ' ')" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
source "./env.d/${target}.env"
|
|
||||||
[ -f ./.env ] && source ./.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
_config_restore "$saved"
|
|
||||||
|
|
||||||
TARGET="$target"
|
|
||||||
|
|
||||||
# Identity is explicit: berth never guesses which estate it is acting on.
|
|
||||||
# The one convenience: a single estate/*.json is used without being asked.
|
|
||||||
if [ -z "${ESTATE:-}" ]; then
|
|
||||||
local n; n=$(ls ../estate/*.json 2>/dev/null | wc -l)
|
|
||||||
if [ "$n" = "1" ]; then
|
|
||||||
ESTATE=$(basename "$(ls ../estate/*.json)" .json)
|
|
||||||
else
|
|
||||||
echo "ESTATE is not set and estate/ holds $n candidates — refusing to guess." >&2
|
|
||||||
echo "available: $(ls ../estate/*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | tr '\n' ' ')" >&2
|
|
||||||
echo "set it: make estate show ESTATE=<name>, or ESTATE= in ctrl/.env" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
ESTATE_FILE="../estate/${ESTATE}.json"
|
|
||||||
if [ ! -f "$ESTATE_FILE" ]; then
|
|
||||||
echo "no such estate: estate/${ESTATE}.json" >&2
|
|
||||||
echo "available: $(ls ../estate/*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | tr '\n' ' ')" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Facts come from the estate file, never restated in a target env.
|
|
||||||
DOMAIN=$(estate_get "domain")
|
|
||||||
HOST="${HOST:-$(estate_get "host")}"
|
|
||||||
HOST_ADMIN="${HOST_ADMIN:-$(estate_get "host_admin")}"
|
|
||||||
|
|
||||||
# Workspace == target, so the two can never mean different things.
|
|
||||||
TOFU_WORKSPACE="${TOFU_WORKSPACE:-$TARGET}"
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
# Reading and projecting estate/<name>.json. Sourced, never executed.
|
|
||||||
#
|
|
||||||
# python3 rather than jq: berth's floor already includes python3, so it is a
|
|
||||||
# dependency berth has rather than one it adds.
|
|
||||||
|
|
||||||
estate_get() {
|
|
||||||
python3 -c '
|
|
||||||
import json, sys
|
|
||||||
d = json.load(open(sys.argv[1]))
|
|
||||||
for k in sys.argv[2].split("."):
|
|
||||||
if isinstance(d, list):
|
|
||||||
try: k = int(k)
|
|
||||||
except ValueError: sys.exit(0)
|
|
||||||
try: d = d[k]
|
|
||||||
except Exception: sys.exit(0)
|
|
||||||
print("" if d is None else d if isinstance(d, str) else json.dumps(d))
|
|
||||||
' "$ESTATE_FILE" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Services in scope for one target. A service names its targets; absent = all.
|
|
||||||
# Fields are US-separated (0x1f), not tab: tab is IFS whitespace, so bash
|
|
||||||
# collapses a run of them and an empty field would shift every later column.
|
|
||||||
estate_services() {
|
|
||||||
local target="${1:-$TARGET}"
|
|
||||||
python3 -c '
|
|
||||||
import json, sys
|
|
||||||
d = json.load(open(sys.argv[1]))
|
|
||||||
target = sys.argv[2]
|
|
||||||
for s in d.get("services", []):
|
|
||||||
tg = s.get("targets")
|
|
||||||
if tg is not None and target not in tg:
|
|
||||||
continue
|
|
||||||
print("\x1f".join([
|
|
||||||
s.get("name", ""),
|
|
||||||
s.get("host", ""),
|
|
||||||
str(s.get(target + "_upstream", s.get("upstream", "")) or ""),
|
|
||||||
s.get("kind", "proxy"),
|
|
||||||
"raw" if s.get("raw") else "",
|
|
||||||
s.get("placement", "box"),
|
|
||||||
s.get("peer", ""),
|
|
||||||
str(s.get("port", "") or ""),
|
|
||||||
s.get("local_host", s.get("host", "")),
|
|
||||||
]))
|
|
||||||
' "$ESTATE_FILE" "$target"
|
|
||||||
}
|
|
||||||
|
|
||||||
# The SAN list the services need, derived — never a literal list.
|
|
||||||
estate_sans() {
|
|
||||||
python3 -c '
|
|
||||||
import json, sys
|
|
||||||
d = json.load(open(sys.argv[1]))
|
|
||||||
domain = d["domain"]
|
|
||||||
sans = [domain]
|
|
||||||
depths = set()
|
|
||||||
for s in d.get("services", []):
|
|
||||||
h = s.get("host", "")
|
|
||||||
if not h:
|
|
||||||
continue
|
|
||||||
# A wildcard matches exactly ONE label. "git" needs *.domain; "dlt.spr"
|
|
||||||
# needs *.spr.domain. The parent of the leaf is what has to be covered.
|
|
||||||
parent = h.split(".", 1)[1] if "." in h else ""
|
|
||||||
depths.add(parent)
|
|
||||||
for p in sorted(depths):
|
|
||||||
sans.append("*." + (p + "." if p else "") + domain)
|
|
||||||
for s in sans:
|
|
||||||
print(s)
|
|
||||||
' "$ESTATE_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Is <fqdn> covered by <san>? A wildcard matches exactly one label.
|
|
||||||
san_covers() {
|
|
||||||
local fqdn="$1" san="$2"
|
|
||||||
[ "$fqdn" = "$san" ] && return 0
|
|
||||||
case "$san" in
|
|
||||||
\*.*)
|
|
||||||
local suffix="${san#\*.}"
|
|
||||||
# Must end in .suffix AND have exactly one extra label.
|
|
||||||
case "$fqdn" in
|
|
||||||
*".$suffix") [ "${fqdn%".$suffix"}" = "${fqdn%%.*}" ] && return 0 ;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── the overlay ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Every overlay name, one per line.
|
|
||||||
overlay_names() {
|
|
||||||
python3 -c '
|
|
||||||
import json, sys
|
|
||||||
d = json.load(open(sys.argv[1]))
|
|
||||||
for n in d.get("vpn", {}).get("overlays", {}):
|
|
||||||
print(n)
|
|
||||||
' "$ESTATE_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Peers of one overlay, US-separated:
|
|
||||||
# name, address, role, endpoint, public_key, allowed_ips, keepalive
|
|
||||||
overlay_peers() {
|
|
||||||
python3 -c '
|
|
||||||
import json, sys
|
|
||||||
d = json.load(open(sys.argv[1]))
|
|
||||||
ov = d.get("vpn", {}).get("overlays", {}).get(sys.argv[2], {})
|
|
||||||
for name, p in ov.get("peers", {}).items():
|
|
||||||
print("\x1f".join(str(x) if x is not None else "" for x in [
|
|
||||||
name, p.get("address"), p.get("role"), p.get("endpoint"),
|
|
||||||
p.get("public_key"), p.get("allowed_ips"), p.get("keepalive"),
|
|
||||||
]))
|
|
||||||
' "$ESTATE_FILE" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay_get() { estate_get "vpn.overlays.$1.$2"; }
|
|
||||||
|
|
||||||
# Is an address inside a CIDR? Pure python so there is no ipcalc dependency —
|
|
||||||
# berth's floor already includes python3 because the IaC side needs it.
|
|
||||||
addr_in_subnet() {
|
|
||||||
python3 -c '
|
|
||||||
import ipaddress, sys
|
|
||||||
try:
|
|
||||||
sys.exit(0 if ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2], strict=False) else 1)
|
|
||||||
except ValueError:
|
|
||||||
sys.exit(2)
|
|
||||||
' "$1" "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
# The upstream a service actually resolves to, as "host:port".
|
|
||||||
#
|
|
||||||
# A PLACED service has no literal `upstream` field: ✖ B9 replaced langfuse's
|
|
||||||
# hand-written `10.8.0.2:3000` with placement+peer+port, because being reached
|
|
||||||
# by address on the overlay is ONE decision, not three properties. Everything
|
|
||||||
# that asks "what does this service point at" must therefore resolve it the
|
|
||||||
# same way, or it silently sees an empty string and skips the service — which
|
|
||||||
# is exactly how vpn.sh's bindings invariant went quiet after B9 landed.
|
|
||||||
#
|
|
||||||
# usage: service_upstream <up> <placement> <peer> <port>
|
|
||||||
service_upstream() {
|
|
||||||
local up="$1" placement="$2" peer="$3" port="$4"
|
|
||||||
case "$placement" in
|
|
||||||
local|instance)
|
|
||||||
local addr; addr="$(overlay_get estate "peers.${peer}.address")"
|
|
||||||
[ -z "$addr" ] && return 1
|
|
||||||
printf '%s:%s' "$addr" "$port"
|
|
||||||
;;
|
|
||||||
*) printf '%s' "$up" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# The local port map, and whether it still agrees with rig.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./ports.sh show # DERIVED / ACTIVE / SOURCE
|
|
||||||
# ./ports.sh verify # recompute rig's formula, report drift
|
|
||||||
#
|
|
||||||
# berth recomputes rig's port formula rather than importing it, so neither
|
|
||||||
# depends on the other. See ../README.md.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
# name<US>host<US>local_port for everything that has one.
|
|
||||||
_local_ports() {
|
|
||||||
python3 -c '
|
|
||||||
import json, sys
|
|
||||||
d = json.load(open(sys.argv[1]))
|
|
||||||
for s in d.get("services", []):
|
|
||||||
p = s.get("local_port")
|
|
||||||
if p:
|
|
||||||
print("\x1f".join([s.get("name",""), s.get("host",""), str(p)]))
|
|
||||||
' "$ESTATE_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
local ld; ld=$(estate_get "local_domain"); : "${ld:=local.ar}"
|
|
||||||
printf '%-12s %-22s %-8s %-8s %s\n' NAME ADDRESS ACTIVE DERIVED SOURCE
|
|
||||||
local name host port base
|
|
||||||
while IFS=$'\x1f' read -r name host port; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
base=$(derive_port_base "$host")
|
|
||||||
if [ "$port" = "$base" ]; then
|
|
||||||
printf '%-12s %-22s %-8s %-8s %s\n' "$name" "${host}.${ld}" "$port" "$base" "derived"
|
|
||||||
else
|
|
||||||
printf '%-12s %-22s %-8s %-8s %s\n' "$name" "${host}.${ld}" "$port" "$base" "override"
|
|
||||||
fi
|
|
||||||
done < <(_local_ports)
|
|
||||||
echo
|
|
||||||
echo "DERIVED is what rig's formula gives for that name. ACTIVE is what the"
|
|
||||||
echo "estate records. 'override' is not an error — most of these were never"
|
|
||||||
echo "rigs. 'make ports verify' says which ones should have matched."
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
|
||||||
local name host port base rc=0 checked=0
|
|
||||||
while IFS=$'\x1f' read -r name host port; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
# Only 20000-21999 is rig's to predict; anything else was never derived.
|
|
||||||
if [ "$port" -lt 20000 ] || [ "$port" -gt 21999 ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
checked=$((checked + 1))
|
|
||||||
base=$(derive_port_base "$host")
|
|
||||||
if [ "$port" != "$base" ]; then
|
|
||||||
echo "DRIFT $name (${host}): estate says $port, rig's formula gives $base"
|
|
||||||
echo " either the rig pinned HTTP_PORT in its ctrl/.env, or the"
|
|
||||||
echo " folder was renamed. The Caddy map is stale either way."
|
|
||||||
rc=1
|
|
||||||
else
|
|
||||||
echo "ok $name (${host}): $port"
|
|
||||||
fi
|
|
||||||
done < <(_local_ports)
|
|
||||||
echo
|
|
||||||
echo "checked $checked rig-shaped port(s) in 20000-21999."
|
|
||||||
[ "$rc" = 0 ] && echo "no drift." || echo "drift found — regenerate with 'make services render local'."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-show}" in
|
|
||||||
show) show ;;
|
|
||||||
verify) verify ;;
|
|
||||||
*) echo "usage: $0 [show|verify]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# The image registry — remote, and reachable only over the overlay.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./registry.sh status
|
|
||||||
#
|
|
||||||
# One verb: berth reports on a registry running on someone else's box. Starting
|
|
||||||
# and stopping it is that box's business.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
status() {
|
|
||||||
local wg_server; wg_server=$(overlay_get estate "peers.box.address")
|
|
||||||
echo "registry: registry.${DOMAIN} (public pull via /v2/)"
|
|
||||||
echo "push: ${wg_server}:5000 (WireGuard-only, not in the firewall)"
|
|
||||||
echo
|
|
||||||
echo "would run:"
|
|
||||||
echo " curl -s https://registry.${DOMAIN}/v2/_catalog"
|
|
||||||
echo
|
|
||||||
echo "NOTE: the push endpoint binds ${wg_server}, and $(estate_get 'vpn._status')."
|
|
||||||
echo " A freshly-provisioned box cannot start the gateway compose file"
|
|
||||||
echo " at all, because that bind fails."
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-status}" in
|
|
||||||
status) status ;;
|
|
||||||
*) echo "usage: $0 [status]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# ${NAME} — GENERATED by berth from estate/${ESTATE}.json. Do not edit.
|
|
||||||
# Edit the estate file and re-run: make services render aws
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name ${FQDN};
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name ${FQDN};
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/live/${DOMAIN}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/live/${DOMAIN}/privkey.pem;
|
|
||||||
|
|
||||||
# Docker's embedded DNS. Naming the upstream in a VARIABLE forces runtime
|
|
||||||
# resolution, so nginx STARTS even when the upstream container is absent.
|
|
||||||
# With a literal proxy_pass, one stopped container takes the whole gateway
|
|
||||||
# down at reload — which is what makes one nginx able to front a dozen
|
|
||||||
# independent compose stacks.
|
|
||||||
resolver 127.0.0.11 valid=30s;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
set $upstream_${NAME} ${UPSTREAM_HOST};
|
|
||||||
proxy_pass http://$upstream_${NAME}:${UPSTREAM_PORT};
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# ${NAME} — GENERATED by berth from estate/${ESTATE}.json. Do not edit.
|
|
||||||
# Edit the estate file and re-run: make services render aws
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name ${FQDN};
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name ${FQDN};
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/live/${DOMAIN}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/live/${DOMAIN}/privkey.pem;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html/${NAME};
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# ${NAME} — GENERATED by berth from estate/${ESTATE}.json. Do not edit.
|
|
||||||
# Placement: ${PLACEMENT} (${PEER}) — reached over the overlay, not the docker network.
|
|
||||||
|
|
||||||
upstream ${NAME}_backend {
|
|
||||||
server ${UPSTREAM_HOST}:${UPSTREAM_PORT};
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name ${FQDN};
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name ${FQDN};
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/live/${DOMAIN}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/live/${DOMAIN}/privkey.pem;
|
|
||||||
|
|
||||||
# No `resolver`, and no `set $var` indirection — deliberately. Those exist so
|
|
||||||
# nginx starts when a CONTAINER is absent; this upstream is a literal address
|
|
||||||
# on the overlay, which needs no DNS at all. The three properties are one
|
|
||||||
# decision, and placement is what decides them.
|
|
||||||
location / {
|
|
||||||
proxy_pass http://${NAME}_backend;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# What berth has settled, and what it has withdrawn, written down as assertions.
|
|
||||||
#
|
|
||||||
# Two halves:
|
|
||||||
# - decisions that hold. Failing one means "you are about to undo this".
|
|
||||||
# - every entry in ../STALE.md. Failing one means a withdrawn assumption came
|
|
||||||
# back. That is the half that makes STALE.md an audit surface and not an
|
|
||||||
# archive — a retraction nobody re-reads is a retraction that decays.
|
|
||||||
#
|
|
||||||
# Scope: no cloud, no ssh, no sudo, no network. Cheap enough to actually run.
|
|
||||||
# `make check` reports on the world and never fails; this exits 1, like rig's.
|
|
||||||
#
|
|
||||||
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
|
||||||
set -uo pipefail # NOT -e: one failing check must not abort the rest
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
|
|
||||||
rc=0
|
|
||||||
passed=0
|
|
||||||
check() { # name, expected, actual
|
|
||||||
if [ "$2" = "$3" ]; then
|
|
||||||
printf ' ok %s\n' "$1"
|
|
||||||
passed=$((passed + 1))
|
|
||||||
else
|
|
||||||
printf ' FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$3"
|
|
||||||
rc=1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
note() { printf '\n%s\n' "$1"; }
|
|
||||||
skip() { printf ' skip %s (%s)\n' "$1" "$2"; }
|
|
||||||
|
|
||||||
# An absence check must not match the files that RECORD the absence. STALE.md
|
|
||||||
# names every withdrawn thing by definition, and this file names them again to
|
|
||||||
# assert them — so both are excluded, or every check fails on itself. rig hits
|
|
||||||
# the same wall and assembles its pattern from fragments for the same reason.
|
|
||||||
NOSELF="--exclude=selftest.sh --exclude=STALE.md"
|
|
||||||
absent() { grep -rIl $NOSELF "$@" 2>/dev/null | wc -l; }
|
|
||||||
|
|
||||||
# A throwaway estate, for the checks that have to run berth rather than read it.
|
|
||||||
TMP_ESTATE=_selftest
|
|
||||||
cleanup() { rm -f "../estate/${TMP_ESTATE}.json"; }
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
note "the withdrawn assumptions — ../STALE.md, one check each"
|
|
||||||
|
|
||||||
# B1 — Pulumi. The two surviving mentions are historical fact about ppl/infra
|
|
||||||
# and live in README.md and the estate, not in anything that runs.
|
|
||||||
check "B1 no pulumi in the code" "0" "$(absent -i pulumi . ../Makefile)"
|
|
||||||
|
|
||||||
# B2 — the ctlptl precedent. Withdrawn; the argument stands on its own now.
|
|
||||||
check "B2 the withdrawn precedent is cited nowhere" "0" "$(absent -i ctlptl ..)"
|
|
||||||
|
|
||||||
# B3 — `wg show <if> dump` leaks the private key in field 1. Stating only
|
|
||||||
# show-vs-showconf makes the dump form read as safe.
|
|
||||||
check "B3 all three wg forms are named" "yes" \
|
|
||||||
"$(grep -q 'dump' vpn.sh && grep -q 'showconf' vpn.sh && echo yes || echo no)"
|
|
||||||
check "B3 capture refuses showconf-shaped input" "1" \
|
|
||||||
"$(printf '[Interface]\nPrivateKey = x\n' | bash vpn.sh capture >/dev/null 2>&1; echo $?)"
|
|
||||||
check "B3 capture refuses dump-shaped input" "1" \
|
|
||||||
"$(printf 'priv\tpub\t51820\toff\n' | bash vpn.sh capture >/dev/null 2>&1; echo $?)"
|
|
||||||
|
|
||||||
# B4 — keepalive belongs to the peer that DIALS, not the one that roams. The
|
|
||||||
# first version warned on a correctly configured overlay, so the check is run
|
|
||||||
# against one: hub carries the keepalive, nrft roams.
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json, collections
|
|
||||||
d = json.load(open("../estate/mcrn.json"), object_pairs_hook=collections.OrderedDict)
|
|
||||||
d["vpn"]["overlays"]["estate"]["peers"]["box"]["keepalive"] = 25
|
|
||||||
json.dump(d, open("../estate/_selftest.json", "w"), indent=2, ensure_ascii=False)
|
|
||||||
PY
|
|
||||||
check "B4 a correct overlay raises no keepalive warning" "0" \
|
|
||||||
"$(ESTATE=$TMP_ESTATE bash vpn.sh check 2>/dev/null | grep -ci 'no peer entry carries')"
|
|
||||||
|
|
||||||
# B6 — public keys are 44-char base64 too, so shape alone would flag correct
|
|
||||||
# data. Same fixture, with a real-shaped public key on a peer.
|
|
||||||
python3 - <<'PY'
|
|
||||||
import base64, collections, json, os
|
|
||||||
d = json.load(open("../estate/_selftest.json"), object_pairs_hook=collections.OrderedDict)
|
|
||||||
d["vpn"]["overlays"]["estate"]["peers"]["box"]["public_key"] = base64.b64encode(os.urandom(32)).decode()
|
|
||||||
json.dump(d, open("../estate/_selftest.json", "w"), indent=2, ensure_ascii=False)
|
|
||||||
PY
|
|
||||||
check "B6 a public key does not trip the secret check" "0" \
|
|
||||||
"$(ESTATE=$TMP_ESTATE bash vpn.sh check 2>/dev/null | grep -c 'FAIL.*key')"
|
|
||||||
|
|
||||||
cleanup # the fixture is done with; two estate files would make load_config
|
|
||||||
# refuse to guess below, which is right but reads as a config failure
|
|
||||||
|
|
||||||
# B5 — the pass-through block must be LAST, or a subcommand that names a real
|
|
||||||
# target runs that target too. Checked through make, not by reading the file.
|
|
||||||
note "B5 a subcommand that names a target dispatches once"
|
|
||||||
for combo in "host ports" "host services" "vpn check" "vpn show estate" "estate show"; do
|
|
||||||
check " make $combo" "1" \
|
|
||||||
"$(cd .. && make -n $combo 2>/dev/null | grep -c 'bash ctrl/')"
|
|
||||||
done
|
|
||||||
|
|
||||||
# B7 — the overlay moved out of network.wireguard into a top-level vpn block.
|
|
||||||
check "B7 nothing reads network.wireguard" "0" "$(absent 'network\.wireguard' .)"
|
|
||||||
|
|
||||||
# B8 — peers, not relatives. berth sources nothing from rig.
|
|
||||||
check "B8 berth sources nothing from rig" "0" "$(absent -E 'rig/ctrl|\.\./rig' .)"
|
|
||||||
|
|
||||||
# B9 — langfuse was filed as an exception a template could not express. It was
|
|
||||||
# the general case. The proof is a live route: render it and diff against the
|
|
||||||
# hand-written file, normalised for comments and whitespace.
|
|
||||||
LIVE=/home/mariano/wdir/semester/ppl/gateway/nginx/conf.d/langfuse.conf
|
|
||||||
if [ -f "$LIVE" ]; then
|
|
||||||
norm() { sed -e 's/#.*//' -e 's/[[:space:]]\+/ /g' -e 's/^ //' -e 's/ $//' -e '/^$/d' "$1"; }
|
|
||||||
bash services.sh render aws >/dev/null 2>&1
|
|
||||||
check "B9 the generated vhost reproduces the live one" "same" \
|
|
||||||
"$(diff -q <(norm ./render/out/aws/langfuse.conf) <(norm "$LIVE") >/dev/null 2>&1 \
|
|
||||||
&& echo same || echo different)"
|
|
||||||
else
|
|
||||||
skip "B9 generated vhost matches the live one" "ppl not on this machine"
|
|
||||||
fi
|
|
||||||
|
|
||||||
note "the safety contract — berth's verbs are not all safe"
|
|
||||||
|
|
||||||
check "estate defaults to show" "show" "$(cd .. && make -n estate 2>/dev/null | grep -oE 'estate\.sh [a-z]+' | awk '{print $2}')"
|
|
||||||
check "certs defaults to status" "status" "$(cd .. && make -n certs 2>/dev/null | grep -oE 'certs\.sh [a-z]+' | awk '{print $2}')"
|
|
||||||
check "dns defaults to list" "list" "$(cd .. && make -n dns 2>/dev/null | grep -oE 'dns\.sh [a-z]+' | awk '{print $2}')"
|
|
||||||
check "vpn defaults to list" "list" "$(cd .. && make -n vpn 2>/dev/null | grep -oE 'vpn\.sh [a-z]+' | awk '{print $2}')"
|
|
||||||
|
|
||||||
for verb in apply destroy; do
|
|
||||||
check "estate $verb refuses without --yes" "1" \
|
|
||||||
"$(bash estate.sh "$verb" >/dev/null 2>&1; echo $?)"
|
|
||||||
done
|
|
||||||
for verb in renew push; do
|
|
||||||
check "certs $verb refuses" "1" \
|
|
||||||
"$(bash certs.sh "$verb" >/dev/null 2>&1; echo $?)"
|
|
||||||
done
|
|
||||||
check "dns add refuses to change live DNS" "1" \
|
|
||||||
"$(bash dns.sh add selftest >/dev/null 2>&1; echo $?)"
|
|
||||||
check "vpn up refuses without --yes" "1" \
|
|
||||||
"$(bash vpn.sh up >/dev/null 2>&1; echo $?)"
|
|
||||||
|
|
||||||
note "config — the caller's env beats the files"
|
|
||||||
|
|
||||||
# Generated from CONFIG_OVERRIDABLE, so a new key enrols itself.
|
|
||||||
test_value() {
|
|
||||||
case "$1" in
|
|
||||||
TARGET) echo "gcp" ;;
|
|
||||||
ESTATE) echo "mcrn" ;;
|
|
||||||
*) echo "selftest-sentinel" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
for key in $CONFIG_OVERRIDABLE; do
|
|
||||||
want="$(test_value "$key")"
|
|
||||||
got="$(export "$key=$want"; load_config >/dev/null 2>&1; echo "${!key}")"
|
|
||||||
check " caller's $key wins" "$want" "$got"
|
|
||||||
done
|
|
||||||
|
|
||||||
note "rig agreement — recomputed, never imported"
|
|
||||||
|
|
||||||
# rig pins these same constants in its own selftest. Both arrive at them from
|
|
||||||
# the same formula with no shared code, which is the coupling rule made testable.
|
|
||||||
check "derive_port_base rig" "20310" "$(derive_port_base rig)"
|
|
||||||
check "derive_port_base foo" "21690" "$(derive_port_base foo)"
|
|
||||||
check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
|
||||||
|
|
||||||
note "containment — berth writes nothing outside berth/"
|
|
||||||
|
|
||||||
check "no tracked change outside berth/" "0" \
|
|
||||||
"$(cd ../.. && git status --porcelain 2>/dev/null | grep -vc '^.. berth/')"
|
|
||||||
check "generated output is ignored" "yes" \
|
|
||||||
"$(cd .. && git check-ignore -q ctrl/render/out && echo yes || echo no)"
|
|
||||||
# A trailing-slash pattern matches directories only, so ask about a path
|
|
||||||
# inside it rather than the (not-yet-existing) directory itself.
|
|
||||||
check "key material is ignored" "yes" \
|
|
||||||
"$(cd .. && git check-ignore -q ctrl/.secrets/vpn/any.key && echo yes || echo no)"
|
|
||||||
|
|
||||||
note "capture and the checks that read it — three bugs found by running, 2026-09-14"
|
|
||||||
|
|
||||||
# 1. A placed service's upstream is DERIVED (✖ B9). Anything reading the raw
|
|
||||||
# `upstream` field sees "" and skips it — which is how vpn.sh's bindings
|
|
||||||
# invariant, the "my configurations broke" detector, went quiet the day
|
|
||||||
# placement landed while still printing OK. Vacuous passes are the failure
|
|
||||||
# mode this whole file exists to catch.
|
|
||||||
check "a placed service resolves to a real upstream" "10.8.0.2:3000" \
|
|
||||||
"$(bash -c 'source ./lib/config.sh; source ./lib/estate.sh; load_config >/dev/null;
|
|
||||||
service_upstream "" local nrft 3000')"
|
|
||||||
check "bindings actually inspects a service" "1" \
|
|
||||||
"$(bash ./vpn.sh check 2>/dev/null | grep -c 'no service currently has an overlay address' \
|
|
||||||
| awk '{print 1-$1}')"
|
|
||||||
|
|
||||||
# 2. A listen port belongs to a PEER. The roaming peer's is an ephemeral source
|
|
||||||
# port; writing it to the overlay renames the port the firewall rule is
|
|
||||||
# checked against — silently, since both are plausible integers.
|
|
||||||
check "a roaming peer's port is not the overlay's port" "51820" \
|
|
||||||
"$(python3 -c 'import json;print(json.load(open("../estate/mcrn.json"))["vpn"]["overlays"]["estate"]["listen_port"])')"
|
|
||||||
|
|
||||||
# 3. _status is always non-empty — capture rewrites it rather than clearing it —
|
|
||||||
# so a warning gated on "is it set" can never turn off, including after the
|
|
||||||
# capture it asks for. Gate on the structure instead.
|
|
||||||
check "the capture warning clears once keys are in" "0" \
|
|
||||||
"$(bash ./check.sh 2>/dev/null | grep -c 'public keys not captured')"
|
|
||||||
|
|
||||||
note "every STALE entry has a check here"
|
|
||||||
|
|
||||||
# Not "$0": line 15 cd's into this script's directory, so a relative $0 no
|
|
||||||
# longer resolves. After the cd the file is simply selftest.sh.
|
|
||||||
# Ids are counted wherever they appear — B5's sits in a note(), not a check name.
|
|
||||||
entries="$(grep -c '^\*\*✖ B' ../STALE.md)"
|
|
||||||
checked="$(grep -oE '\bB[1-9][0-9]?\b' selftest.sh | sort -u | wc -l)"
|
|
||||||
check "STALE.md entries are all covered" "$entries" "$checked"
|
|
||||||
|
|
||||||
printf '\n%d passed' "$passed"
|
|
||||||
[ "$rc" -ne 0 ] && printf ', SOME FAILED'
|
|
||||||
printf '\n'
|
|
||||||
exit "$rc"
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Gateway routes, projected from the estate onto one target.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./services.sh # list
|
|
||||||
# ./services.sh render aws # -> render/out/aws/*.conf (nginx vhosts)
|
|
||||||
# ./services.sh render local # -> render/out/local/Caddyfile
|
|
||||||
# ./services.sh deploy # refuses; ppl/ctrl/deploy.sh ships config
|
|
||||||
#
|
|
||||||
# Each target is a projection with its own rules, not a format conversion.
|
|
||||||
# The nine axes they disagree on, and the install order: ../README.md
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
OUT_ROOT="./render/out"
|
|
||||||
|
|
||||||
list() {
|
|
||||||
printf '%-12s %-16s %-22s %-9s %s\n' NAME FQDN UPSTREAM PLACEMENT SOURCE
|
|
||||||
local name host up kind raw
|
|
||||||
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
# A placed service has no literal `upstream` — it is derived from the
|
|
||||||
# peer's overlay address, so show what it actually resolves to.
|
|
||||||
local shown; shown="$(service_upstream "$up" "$placement" "$peer" "$port")" || shown=""
|
|
||||||
printf '%-12s %-16s %-22s %-9s %s\n' \
|
|
||||||
"$name" "${host}.${DOMAIN}" "${shown:--}" "$placement" \
|
|
||||||
"$([ -n "$raw" ] && echo 'hand-written' || echo 'generated')"
|
|
||||||
done < <(estate_services "$TARGET")
|
|
||||||
echo
|
|
||||||
echo "hand-written entries are NOT generated and NOT overwritten."
|
|
||||||
echo "run 'make estate show' to see why each one is an exception."
|
|
||||||
}
|
|
||||||
|
|
||||||
render_cloud() {
|
|
||||||
# Two statements: `local a="$1" b="$a"` expands all arguments before any
|
|
||||||
# assignment, so $a would still be unset.
|
|
||||||
local target="$1"
|
|
||||||
local out="$OUT_ROOT/$target"
|
|
||||||
rm -rf "$out"; mkdir -p "$out"
|
|
||||||
local name host up kind raw uhost uport n=0 skipped=0
|
|
||||||
|
|
||||||
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
if [ -n "$raw" ]; then
|
|
||||||
skipped=$((skipped + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Placement picks the rendering. A container on the estate's own network
|
|
||||||
# is reached by NAME through docker's resolver; anything on the overlay
|
|
||||||
# is reached by ADDRESS and needs no DNS. That is one decision, not the
|
|
||||||
# three properties (upstream{}, no resolver, no set $var) it produces.
|
|
||||||
local tmpl
|
|
||||||
case "$placement" in
|
|
||||||
local|instance)
|
|
||||||
tmpl=./render/nginx-upstream.tmpl
|
|
||||||
uhost="$(overlay_get estate "peers.${peer}.address")"
|
|
||||||
uport="$port" # resolved via service_upstream's same rule
|
|
||||||
if [ -z "$uhost" ]; then
|
|
||||||
echo " ! $name: placement '$placement' names peer '$peer', which has no address" >&2
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
hosted)
|
|
||||||
echo " ! $name: placement 'hosted' is declared but not rendered yet" >&2
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [ "$kind" = "static" ]; then
|
|
||||||
tmpl=./render/nginx-static.tmpl
|
|
||||||
uhost=""; uport=""
|
|
||||||
else
|
|
||||||
tmpl=./render/nginx-proxy.tmpl
|
|
||||||
uhost="${up%%:*}"; uport="${up##*:}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
sed -e "s|\${NAME}|${name}|g" \
|
|
||||||
-e "s|\${ESTATE}|${ESTATE}|g" \
|
|
||||||
-e "s|\${FQDN}|${host}.${DOMAIN}|g" \
|
|
||||||
-e "s|\${DOMAIN}|${DOMAIN}|g" \
|
|
||||||
-e "s|\${PLACEMENT}|${placement}|g" \
|
|
||||||
-e "s|\${PEER}|${peer}|g" \
|
|
||||||
-e "s|\${UPSTREAM_HOST}|${uhost}|g" \
|
|
||||||
-e "s|\${UPSTREAM_PORT}|${uport}|g" \
|
|
||||||
"$tmpl" > "$out/${name}.conf"
|
|
||||||
n=$((n + 1))
|
|
||||||
done < <(estate_services "$target")
|
|
||||||
|
|
||||||
echo "wrote $n vhost(s) to $out/ ($skipped hand-written, left alone)"
|
|
||||||
cat <<EONOTE
|
|
||||||
|
|
||||||
TO INSTALL THESE, THREE THINGS MUST HAPPEN IN THIS ORDER — and the order is the
|
|
||||||
whole reason this is not a one-liner:
|
|
||||||
|
|
||||||
1. These land in conf.d/generated/, NOT conf.d/. ppl/ctrl/deploy.sh rsyncs the
|
|
||||||
gateway with --delete; generated and hand-written config sharing one
|
|
||||||
directory means one of them gets erased.
|
|
||||||
|
|
||||||
2. nginx.conf needs a THIRD include line. Its conf.d/*.conf glob does not
|
|
||||||
recurse — which is exactly why conf.d/soleprint/*.conf already needs its
|
|
||||||
own line at nginx.conf:28-30.
|
|
||||||
|
|
||||||
3. That new include changes LOAD ORDER, and load order decides which :443
|
|
||||||
block catches unmatched names. So default.conf's commented-out
|
|
||||||
':443 default_server' must be restored FIRST. 'make check' fails on this
|
|
||||||
today, deliberately — it is a gate, not a warning.
|
|
||||||
EONOTE
|
|
||||||
}
|
|
||||||
|
|
||||||
render_local() {
|
|
||||||
local out="$OUT_ROOT/local"; mkdir -p "$out"
|
|
||||||
local ld; ld=$(estate_get "local_domain")
|
|
||||||
: "${ld:=local.ar}"
|
|
||||||
local name host up kind raw port drift=0
|
|
||||||
|
|
||||||
{
|
|
||||||
cat <<EOH
|
|
||||||
# GENERATED by berth from estate/${ESTATE}.json. Do not edit.
|
|
||||||
# Regenerate: make services render local
|
|
||||||
#
|
|
||||||
# Install: sudo ln -sf \$PWD/Caddyfile /etc/caddy/Caddyfile && sudo systemctl reload caddy
|
|
||||||
# All *.${ld} resolve to 127.0.0.1 via dnsmasq.
|
|
||||||
#
|
|
||||||
# Every site address carries an explicit :80. Without it Caddy 2 defaults to
|
|
||||||
# :443 with auto-HTTPS, which on *.${ld} means cert provisioning attempts that
|
|
||||||
# fail and break the listener. Plain HTTP only on this host.
|
|
||||||
#
|
|
||||||
# Caddy matches the MOST SPECIFIC site address, not the first — the opposite of
|
|
||||||
# nginx, which matches exactly and otherwise falls to default_server. A name set
|
|
||||||
# that is unambiguous here can be ambiguous on the box.
|
|
||||||
EOH
|
|
||||||
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
port=$(python3 -c '
|
|
||||||
import json,sys
|
|
||||||
d=json.load(open(sys.argv[1]))
|
|
||||||
for s in d.get("services",[]):
|
|
||||||
if s.get("name")==sys.argv[2]:
|
|
||||||
print(s.get("local_port") or ""); break
|
|
||||||
' "$ESTATE_FILE" "$name")
|
|
||||||
[ -z "$port" ] && continue
|
|
||||||
echo
|
|
||||||
echo "${lhost}.${ld}:80, *.${lhost}.${ld}:80 {"
|
|
||||||
echo " reverse_proxy localhost:${port}"
|
|
||||||
echo "}"
|
|
||||||
done < <(estate_services local)
|
|
||||||
} > "$out/Caddyfile"
|
|
||||||
|
|
||||||
echo "wrote $out/Caddyfile"
|
|
||||||
echo
|
|
||||||
echo "rig is not consulted and does not know this exists — its handover"
|
|
||||||
echo "scrub refuses the string '${ld}'. Where a port belongs to a rig,"
|
|
||||||
echo "'make ports verify' RECOMPUTES rig's formula to check it rather than"
|
|
||||||
echo "importing rig's code. Convention, verified; not a dependency."
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-list}" in
|
|
||||||
list) list ;;
|
|
||||||
render)
|
|
||||||
shift
|
|
||||||
# `case "${1:-X}"` defaults the match but leaves $1 empty.
|
|
||||||
t="${1:-$TARGET}"
|
|
||||||
case "$t" in
|
|
||||||
local) render_local ;;
|
|
||||||
aws|gcp) render_cloud "$t" ;;
|
|
||||||
*) echo "usage: $0 render [aws|gcp|local]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
deploy)
|
|
||||||
echo "berth does not ship config; ppl/ctrl/deploy.sh does." >&2
|
|
||||||
echo " berth's half is the DESCRIPTION and the render. Shipping is" >&2
|
|
||||||
echo " rsync + compose against a live box, and it belongs where the" >&2
|
|
||||||
echo " credentials are: berth is the tool, ppl is the estate that" >&2
|
|
||||||
echo " holds the secrets." >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
*) echo "usage: $0 [list|render [aws|gcp|local]|deploy]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Pinned toolchain. Committed. The weakest config layer.
|
|
||||||
|
|
||||||
# The infra executor. One, not a pair — see README.md.
|
|
||||||
# This pin is a placeholder; set it from `tofu version` once installed.
|
|
||||||
TOFU_VERSION=1.9.0
|
|
||||||
TOFU_BIN=tofu
|
|
||||||
|
|
||||||
# certbot runs as a throwaway container so the DNS plugin's credentials never
|
|
||||||
# have to be installed on this machine.
|
|
||||||
CERTBOT_AWS_IMAGE=certbot/dns-route53:latest
|
|
||||||
CERTBOT_GCP_IMAGE=certbot/dns-google:latest
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Overlays — WireGuard as berth's network layer.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./vpn.sh # list
|
|
||||||
# ./vpn.sh show <overlay> # topology
|
|
||||||
# ./vpn.sh check # invariants
|
|
||||||
# ./vpn.sh render <peer> # peer's wg0.conf -> render/out/vpn/
|
|
||||||
# ./vpn.sh keygen <peer> # keypair -> .secrets/; prints only the public key
|
|
||||||
# ./vpn.sh up|down --yes # refuses; the host operates its own tunnel
|
|
||||||
#
|
|
||||||
# sudo wg show | ./vpn.sh capture [--write]
|
|
||||||
#
|
|
||||||
# `wg show` is the only safe form: `wg show <if> dump` puts the private key in
|
|
||||||
# field 1, and `wg showconf` prints it outright. capture refuses both.
|
|
||||||
#
|
|
||||||
# Rationale, topology and key handling: ../README.md
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
source ./lib/estate.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
SECRETS_DIR="./.secrets/vpn"
|
|
||||||
OUT_DIR="./render/out/vpn"
|
|
||||||
|
|
||||||
WORST=0
|
|
||||||
note() { echo " $*"; }
|
|
||||||
warn() { echo " WARN $*"; [ "$WORST" -lt 1 ] && WORST=1; return 0; }
|
|
||||||
bad() { echo " FAIL $*"; WORST=2; return 0; }
|
|
||||||
|
|
||||||
# Which addresses belong to THIS machine, so checks can distinguish what they
|
|
||||||
# can actually see from what needs capturing elsewhere.
|
|
||||||
my_overlay_addrs() { ip -4 -o addr show 2>/dev/null | awk '{split($4,a,"/"); print a[1]}'; }
|
|
||||||
is_me() { my_overlay_addrs | grep -qxF "$1"; }
|
|
||||||
|
|
||||||
list() {
|
|
||||||
local n sub port peers
|
|
||||||
for n in $(overlay_names); do
|
|
||||||
sub=$(overlay_get "$n" subnet)
|
|
||||||
port=$(overlay_get "$n" listen_port)
|
|
||||||
peers=$(overlay_peers "$n" | grep -c . || true)
|
|
||||||
printf '%-10s %-16s port %-7s %s peer(s)\n' "$n" "$sub" "$port" "$peers"
|
|
||||||
note "$(overlay_get "$n" purpose)"
|
|
||||||
done
|
|
||||||
local st; st=$(estate_get "vpn._status")
|
|
||||||
[ -n "$st" ] && { echo; echo " !! $st"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
local ov="${1:-}"
|
|
||||||
[ -z "$ov" ] && { echo "usage: $0 show <overlay>" >&2; exit 1; }
|
|
||||||
overlay_names | grep -qxF "$ov" || {
|
|
||||||
echo "no such overlay: $ov" >&2
|
|
||||||
echo "available: $(overlay_names | tr '\n' ' ')" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "overlay: $ov subnet $(overlay_get "$ov" subnet) udp/$(overlay_get "$ov" listen_port)"
|
|
||||||
echo
|
|
||||||
printf '%-8s %-12s %-9s %-22s %s\n' PEER ADDRESS ROLE ENDPOINT PUBKEY
|
|
||||||
local name addr role ep pk aips ka
|
|
||||||
while IFS=$'\x1f' read -r name addr role ep pk aips ka; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
printf '%-8s %-12s %-9s %-22s %s%s\n' \
|
|
||||||
"$name" "$addr" "$role" "${ep:-—}" "${pk:-—}" \
|
|
||||||
"$(is_me "$addr" && echo ' <- this machine')"
|
|
||||||
done < <(overlay_peers "$ov")
|
|
||||||
}
|
|
||||||
|
|
||||||
check() {
|
|
||||||
local ov name addr role ep pk aips ka
|
|
||||||
for ov in $(overlay_names); do
|
|
||||||
local sub port
|
|
||||||
sub=$(overlay_get "$ov" subnet); port=$(overlay_get "$ov" listen_port)
|
|
||||||
echo "overlay '$ov' — $sub udp/$port"
|
|
||||||
|
|
||||||
# 1. addresses: unique, and inside the subnet. Two peers sharing an
|
|
||||||
# address is a silent misroute, never an error message.
|
|
||||||
local addrs; addrs=$(overlay_peers "$ov" | cut -d$'\x1f' -f2 | grep -v '^$' || true)
|
|
||||||
local dupes; dupes=$(echo "$addrs" | sort | uniq -d)
|
|
||||||
[ -n "$dupes" ] && bad "duplicate peer addresses: $(echo "$dupes" | tr '\n' ' ')"
|
|
||||||
while IFS= read -r a; do
|
|
||||||
[ -z "$a" ] && continue
|
|
||||||
addr_in_subnet "$a" "$sub" || bad "$a is outside $sub"
|
|
||||||
done <<< "$addrs"
|
|
||||||
|
|
||||||
while IFS=$'\x1f' read -r name addr role ep pk aips ka; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
|
|
||||||
# AllowedIPs is cryptokey routing — route table and ACL at once.
|
|
||||||
case "$aips" in
|
|
||||||
*0.0.0.0/0*) warn "$name: AllowedIPs includes 0.0.0.0/0 — full-tunnel. Deliberate?" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# A peer with no endpoint cannot be dialed; it must initiate.
|
|
||||||
if [ -z "$ep" ] && [ "$role" != "roaming" ]; then
|
|
||||||
warn "$name: role '$role' but no endpoint — nothing can dial it."
|
|
||||||
fi
|
|
||||||
# Keepalive is NOT on the roaming peer's own entry — it is set on
|
|
||||||
# the entry for the peer it dials. Checked per-overlay below.
|
|
||||||
[ -z "$pk" ] && note "$name: public_key not captured yet"
|
|
||||||
done < <(overlay_peers "$ov")
|
|
||||||
|
|
||||||
# If anything roams, some peer entry must carry a keepalive.
|
|
||||||
if overlay_peers "$ov" | cut -d$'\x1f' -f3 | grep -qx roaming; then
|
|
||||||
if ! overlay_peers "$ov" | cut -d$'\x1f' -f7 | grep -qE '^[0-9]+$'; then
|
|
||||||
warn "a peer roams but no peer entry carries PersistentKeepalive"
|
|
||||||
note " the roaming side sets it on the entry for the peer it dials;"
|
|
||||||
note " without it the NAT mapping expires and the tunnel works only"
|
|
||||||
note " while traffic flows outward — 'works sometimes'"
|
|
||||||
else
|
|
||||||
note "keepalive present on the dialed peer."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. the listen port must be open wherever a peer is dialable.
|
|
||||||
local fwports; fwports=$(estate_get "firewall" | python3 -c '
|
|
||||||
import json,sys
|
|
||||||
try: print(" ".join(str(r.get("port")) for r in json.load(sys.stdin)))
|
|
||||||
except Exception: pass')
|
|
||||||
case " $fwports " in
|
|
||||||
*" $port "*) note "udp/$port present in the firewall description." ;;
|
|
||||||
*) bad "udp/$port is in no firewall rule — no peer could be dialed." ;;
|
|
||||||
esac
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
|
|
||||||
# Public keys are also 44-char base64, so shape alone proves nothing. The
|
|
||||||
# assertions are: no field named private, no key outside a public_key field.
|
|
||||||
echo "secrets — the description must never carry a private key"
|
|
||||||
local leaked
|
|
||||||
leaked=$(python3 - estate/../../estate/*.json <<'PY' 2>/dev/null || true
|
|
||||||
import json, re, sys, glob
|
|
||||||
KEY = re.compile(r'^[A-Za-z0-9+/]{43}=$')
|
|
||||||
bad = []
|
|
||||||
for f in glob.glob("../estate/*.json"):
|
|
||||||
def walk(node, path):
|
|
||||||
if isinstance(node, dict):
|
|
||||||
for k, v in node.items():
|
|
||||||
if re.search(r'priv', k, re.I):
|
|
||||||
bad.append(f"{f}: field '{'.'.join(path+[k])}' is named private")
|
|
||||||
walk(v, path + [k])
|
|
||||||
elif isinstance(node, list):
|
|
||||||
for i, v in enumerate(node): walk(v, path + [str(i)])
|
|
||||||
elif isinstance(node, str) and KEY.match(node):
|
|
||||||
if not path or 'public' not in path[-1]:
|
|
||||||
bad.append(f"{f}: base64 key at '{'.'.join(path)}' is not a public_key field")
|
|
||||||
walk(json.load(open(f)), [])
|
|
||||||
print("\n".join(bad))
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
if [ -n "$leaked" ]; then
|
|
||||||
echo "$leaked" | while IFS= read -r l; do [ -n "$l" ] && bad "$l"; done
|
|
||||||
else
|
|
||||||
note "clean — no private-named field, no stray key material."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# A service reached over the overlay must bind an address the tunnel can
|
|
||||||
# reach. Loopback cannot be reached through a tunnel.
|
|
||||||
echo "bindings — services reached over the overlay must bind a reachable address"
|
|
||||||
local checked=0
|
|
||||||
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
||||||
# Resolve placement first: a placed service's upstream is derived, not
|
|
||||||
# literal, so reading `up` alone skips it and this invariant goes quiet.
|
|
||||||
up="$(service_upstream "$up" "$placement" "$peer" "$port")" || true
|
|
||||||
[ -z "$up" ] && continue
|
|
||||||
local uhost="${up%%:*}" uport="${up##*:}"
|
|
||||||
addr_in_subnet "$uhost" "$(overlay_get estate subnet)" 2>/dev/null || continue
|
|
||||||
checked=$((checked + 1))
|
|
||||||
if is_me "$uhost"; then
|
|
||||||
local binds; binds=$(ss -ltn 2>/dev/null | awk -v p=":$uport\$" '$4 ~ p {print $4}')
|
|
||||||
if [ -z "$binds" ]; then
|
|
||||||
bad "$name: nothing listens on :$uport here, but $uhost:$uport is its upstream"
|
|
||||||
elif echo "$binds" | grep -q '^127\.0\.0\.1:'; then
|
|
||||||
bad "$name: :$uport binds 127.0.0.1 — unreachable over the overlay"
|
|
||||||
note " the tunnel cannot reach loopback; bind 0.0.0.0 or $uhost"
|
|
||||||
else
|
|
||||||
note "$name: :$uport binds $(echo "$binds" | tr '\n' ' ')— reachable"
|
|
||||||
echo "$binds" | grep -q '^0\.0\.0\.0:' && \
|
|
||||||
note " (0.0.0.0 also exposes it to the LAN; $uhost alone would be tighter)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
note "$name: upstream $uhost is another peer — needs capture there"
|
|
||||||
fi
|
|
||||||
done < <(estate_services "$TARGET")
|
|
||||||
[ "$checked" = 0 ] && note "no service currently has an overlay address as its upstream."
|
|
||||||
echo
|
|
||||||
|
|
||||||
case "$WORST" in
|
|
||||||
0) echo "OK" ;;
|
|
||||||
1) echo "OK, with warnings" ;;
|
|
||||||
2) echo "PROBLEMS FOUND — see FAIL lines above" ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# A .gitignore pattern containing a slash anchors to its own directory, so the
|
|
||||||
# only way to know a path is ignored is to ask git.
|
|
||||||
assert_ignored() {
|
|
||||||
local path="$1"
|
|
||||||
if ! git check-ignore -q "$path" 2>/dev/null; then
|
|
||||||
echo "REFUSING: '$path' is not gitignored." >&2
|
|
||||||
echo " Writing key material there would stage it on the next 'git add'." >&2
|
|
||||||
echo " Verify with: git check-ignore -v $path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
keygen() {
|
|
||||||
local peer="${1:-}"
|
|
||||||
[ -z "$peer" ] && { echo "usage: $0 keygen <peer>" >&2; exit 1; }
|
|
||||||
command -v wg >/dev/null || { echo "wg not installed." >&2; exit 1; }
|
|
||||||
|
|
||||||
mkdir -p "$SECRETS_DIR"
|
|
||||||
assert_ignored "$SECRETS_DIR"
|
|
||||||
local kf="$SECRETS_DIR/${peer}.key"
|
|
||||||
[ -e "$kf" ] && { echo "REFUSING: $kf exists. Delete it deliberately to rotate." >&2; exit 1; }
|
|
||||||
|
|
||||||
( umask 077; wg genkey > "$kf" )
|
|
||||||
echo "private key -> $kf (0600, gitignored, never leaves this machine)"
|
|
||||||
echo
|
|
||||||
echo "public key for the estate description:"
|
|
||||||
echo " $(wg pubkey < "$kf")"
|
|
||||||
echo
|
|
||||||
echo "Paste that into estate/*.json under vpn.overlays.<ov>.peers.${peer}.public_key."
|
|
||||||
echo "The private key stays here and is injected only at render time."
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
local peer="${1:-}"
|
|
||||||
[ -z "$peer" ] && { echo "usage: $0 render <peer>" >&2; exit 1; }
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
assert_ignored "$OUT_DIR"
|
|
||||||
|
|
||||||
local ov=estate
|
|
||||||
local found=""
|
|
||||||
local name addr role ep pk aips ka
|
|
||||||
while IFS=$'\x1f' read -r name addr role ep pk aips ka; do
|
|
||||||
[ "$name" = "$peer" ] && found=1 && break
|
|
||||||
done < <(overlay_peers "$ov")
|
|
||||||
[ -z "$found" ] && { echo "no such peer '$peer' in overlay '$ov'" >&2; exit 1; }
|
|
||||||
|
|
||||||
local missing=""
|
|
||||||
while IFS=$'\x1f' read -r name addr role ep pk aips ka; do
|
|
||||||
[ -z "$pk" ] && missing="$missing $name"
|
|
||||||
done < <(overlay_peers "$ov")
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
echo "REFUSING to render: public keys not captured for:$missing" >&2
|
|
||||||
echo " A config without every peer's public key is a config that silently" >&2
|
|
||||||
echo " drops those peers. Capture first: sudo wg show | $0 capture --write" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "would write $OUT_DIR/${peer}.conf (all keys present)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reads `wg show` on stdin. Peers match by allowed-ips address, not public key,
|
|
||||||
# because the keys are what is missing. A roaming peer's endpoint is a home
|
|
||||||
# address and has no stable value — dropped in the parser, not just unused.
|
|
||||||
capture() {
|
|
||||||
local write="" as_peer=""
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--write) write=1 ;;
|
|
||||||
--as) shift; as_peer="${1:-}"
|
|
||||||
[ -z "$as_peer" ] && { echo "--as needs a peer name" >&2; exit 1; } ;;
|
|
||||||
*) echo "capture: unknown argument '$1'" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
local input; input=$(cat)
|
|
||||||
if [ -z "$input" ]; then
|
|
||||||
echo "nothing on stdin." >&2
|
|
||||||
echo " run: sudo wg show | $0 capture" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Refuse the unsafe forms outright rather than parsing around them.
|
|
||||||
if printf '%s' "$input" | grep -qiE '^\s*PrivateKey\s*=|^\[Interface\]'; then
|
|
||||||
echo "REFUSING: this looks like 'wg showconf' output — it contains a PRIVATE KEY." >&2
|
|
||||||
echo " Use 'sudo wg show' (plain). It prints 'private key: (hidden)'." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! printf '%s' "$input" | grep -q 'interface:'; then
|
|
||||||
echo "REFUSING: this does not look like 'wg show' output." >&2
|
|
||||||
echo " If it was 'wg show <if> dump': that form's first field IS the" >&2
|
|
||||||
echo " private key. Use 'sudo wg show' with no subcommand." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
WRITE="$write" AS_PEER="$as_peer" INPUT="$input" python3 - "$ESTATE_FILE" <<'PYCAP'
|
|
||||||
import collections, ipaddress, json, os, re, sys
|
|
||||||
|
|
||||||
text = os.environ["INPUT"]
|
|
||||||
write = os.environ.get("WRITE") == "1"
|
|
||||||
path = sys.argv[1]
|
|
||||||
|
|
||||||
iface, peers, cur = {}, [], None
|
|
||||||
for line in text.splitlines():
|
|
||||||
st = line.strip()
|
|
||||||
if st.startswith("interface:"):
|
|
||||||
cur = iface; cur["name"] = st.split(":", 1)[1].strip(); continue
|
|
||||||
if st.startswith("peer:"):
|
|
||||||
cur = {"public_key": st.split(":", 1)[1].strip()}; peers.append(cur); continue
|
|
||||||
if cur is None or ":" not in st:
|
|
||||||
continue
|
|
||||||
k, v = st.split(":", 1)
|
|
||||||
k, v = k.strip().lower(), v.strip()
|
|
||||||
if k == "private key":
|
|
||||||
continue # never recorded, whatever it says
|
|
||||||
if k == "public key": cur["public_key"] = v
|
|
||||||
elif k == "listening port": cur["listen_port"] = v
|
|
||||||
elif k == "allowed ips": cur["allowed_ips"] = v
|
|
||||||
elif k == "endpoint": cur["endpoint"] = v
|
|
||||||
elif k == "persistent keepalive":
|
|
||||||
m = re.search(r"(\d+)", v)
|
|
||||||
if m: cur["keepalive"] = int(m.group(1))
|
|
||||||
|
|
||||||
d = json.load(open(path), object_pairs_hook=collections.OrderedDict)
|
|
||||||
ov = d["vpn"]["overlays"]["estate"]
|
|
||||||
|
|
||||||
# address -> peer name, from what the estate already declares
|
|
||||||
by_addr = {p["address"]: n for n, p in ov["peers"].items() if p.get("address")}
|
|
||||||
by_key = {p["public_key"]: n for n, p in ov["peers"].items() if p.get("public_key")}
|
|
||||||
subnet = ipaddress.ip_network(ov["subnet"]) if ov.get("subnet") else None
|
|
||||||
hubs = [n for n, p in ov["peers"].items() if p.get("role") == "hub"]
|
|
||||||
|
|
||||||
# Whose interface block is this? `--as` names it explicitly, and that is the only
|
|
||||||
# thing that works for output captured over ssh: the addresses on THIS machine
|
|
||||||
# say nothing about the machine the output came from.
|
|
||||||
as_peer = os.environ.get("AS_PEER") or ""
|
|
||||||
if as_peer:
|
|
||||||
if as_peer not in ov["peers"]:
|
|
||||||
print("no peer named %r in this overlay. known: %s"
|
|
||||||
% (as_peer, ", ".join(ov["peers"])))
|
|
||||||
raise SystemExit(1)
|
|
||||||
me = as_peer
|
|
||||||
else:
|
|
||||||
me = None
|
|
||||||
local = os.popen(
|
|
||||||
"ip -4 -o addr show 2>/dev/null | awk '{split($4,a,\"/\"); print a[1]}'"
|
|
||||||
).read().split()
|
|
||||||
for n, p in ov["peers"].items():
|
|
||||||
if p.get("address") and p["address"] in local:
|
|
||||||
me = n
|
|
||||||
|
|
||||||
changes = []
|
|
||||||
conflicts = []
|
|
||||||
staged = {}
|
|
||||||
def setf(peer, field, val, why=""):
|
|
||||||
p = ov["peers"][peer]
|
|
||||||
if val is None or p.get(field) == val:
|
|
||||||
return
|
|
||||||
# Two values for one field means the input is from another machine.
|
|
||||||
prev = staged.get((peer, field))
|
|
||||||
if prev is not None and prev != val:
|
|
||||||
conflicts.append((peer, field, prev, val))
|
|
||||||
return
|
|
||||||
staged[(peer, field)] = val
|
|
||||||
changes.append((peer, field, p.get(field), val, why))
|
|
||||||
if write:
|
|
||||||
p[field] = val
|
|
||||||
|
|
||||||
if me and iface.get("public_key"):
|
|
||||||
setf(me, "public_key", iface["public_key"], "(this machine's interface)")
|
|
||||||
|
|
||||||
def match(pr):
|
|
||||||
# 1. The public key IS the identity. Use it whenever the estate knows it.
|
|
||||||
n = by_key.get(pr["public_key"])
|
|
||||||
if n:
|
|
||||||
return n
|
|
||||||
nets = [a.strip() for a in pr.get("allowed_ips", "").split(",") if a.strip()]
|
|
||||||
# 2. An allowed-ip that is a declared peer address — the ordinary spoke case.
|
|
||||||
for a in nets:
|
|
||||||
if a.split("/")[0] in by_addr:
|
|
||||||
return by_addr[a.split("/")[0]]
|
|
||||||
# 3. A peer routing the WHOLE overlay is the hub seen from a spoke. Its
|
|
||||||
# allowed_ips is the subnet itself, so no single address ever matches it.
|
|
||||||
if subnet and len(hubs) == 1:
|
|
||||||
for a in nets:
|
|
||||||
try:
|
|
||||||
if ipaddress.ip_network(a, strict=False).supernet_of(subnet):
|
|
||||||
return hubs[0]
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
|
|
||||||
for pr in peers:
|
|
||||||
name = match(pr)
|
|
||||||
if not name:
|
|
||||||
changes.append(("?", "UNMATCHED", None,
|
|
||||||
"allowed_ips=%s key=%s" % (pr.get("allowed_ips"), pr["public_key"][:12] + "..."),
|
|
||||||
"no estate peer has this key, this address, or this route"))
|
|
||||||
continue
|
|
||||||
setf(name, "public_key", pr.get("public_key"))
|
|
||||||
setf(name, "allowed_ips", pr.get("allowed_ips"))
|
|
||||||
setf(name, "keepalive", pr.get("keepalive"))
|
|
||||||
# endpoint: recorded ONLY for a non-roaming peer. For a roaming one the
|
|
||||||
# value is a home ISP address and is deliberately dropped here.
|
|
||||||
if pr.get("endpoint"):
|
|
||||||
if ov["peers"][name].get("role") == "roaming":
|
|
||||||
changes.append((name, "endpoint", None, "(dropped: roaming peer)",
|
|
||||||
"a home address is the one sensitive field; roaming peers have no stable endpoint"))
|
|
||||||
else:
|
|
||||||
setf(name, "endpoint", pr["endpoint"])
|
|
||||||
|
|
||||||
if me and iface.get("listen_port"):
|
|
||||||
try:
|
|
||||||
lp = int(iface["listen_port"])
|
|
||||||
except ValueError:
|
|
||||||
lp = None
|
|
||||||
if lp is not None:
|
|
||||||
# A listen port belongs to the PEER, not to the overlay. A roaming peer's
|
|
||||||
# is an ephemeral source port chosen by the kernel; writing it to the
|
|
||||||
# overlay would rename the port the firewall rule is checked against.
|
|
||||||
setf(me, "listen_port", lp)
|
|
||||||
if ov["peers"][me].get("role") == "hub" and ov.get("listen_port") != lp:
|
|
||||||
changes.append(("(overlay)", "listen_port", ov.get("listen_port"), lp,
|
|
||||||
"the hub's port is the overlay's port"))
|
|
||||||
if write:
|
|
||||||
ov["listen_port"] = lp
|
|
||||||
|
|
||||||
if conflicts:
|
|
||||||
print("REFUSING: the same field was reported twice with different values.\n")
|
|
||||||
for peer, field, a, b in conflicts:
|
|
||||||
print(" %s.%s: %s vs %s" % (peer, field, a, b))
|
|
||||||
print("\nThis usually means `wg show` output from one machine was piped into")
|
|
||||||
print("capture on another. Run capture on the machine the output came from.")
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
if not changes:
|
|
||||||
print("nothing to record — the estate already matches what wg reports.")
|
|
||||||
else:
|
|
||||||
print("%-9s %-12s %-22s %s" % ("PEER", "FIELD", "WAS", "WOULD BE"))
|
|
||||||
for peer, field, was, val, why in changes:
|
|
||||||
print("%-9s %-12s %-22s %s" % (peer, field, was if was is not None else "—", val))
|
|
||||||
if why: print(" %s" % why)
|
|
||||||
|
|
||||||
if write:
|
|
||||||
still = [n for n, p in ov["peers"].items() if not p.get("public_key")]
|
|
||||||
if not still:
|
|
||||||
d["vpn"]["_status"] = ("CAPTURED %s — public keys, allowed-ips and keepalive read from "
|
|
||||||
"`wg show`. Roaming endpoints deliberately not recorded."
|
|
||||||
% __import__("datetime").date.today())
|
|
||||||
json.dump(d, open(path, "w"), indent=2, ensure_ascii=False)
|
|
||||||
open(path, "a").write("\n")
|
|
||||||
print("\nwritten to %s" % path)
|
|
||||||
else:
|
|
||||||
print("\nnothing written. Add --write to record it.")
|
|
||||||
PYCAP
|
|
||||||
}
|
|
||||||
|
|
||||||
refuse() {
|
|
||||||
local verb="$1"; shift
|
|
||||||
local yes=""
|
|
||||||
for a in "$@"; do [ "$a" = "--yes" ] && yes=1; done
|
|
||||||
[ -z "$yes" ] && {
|
|
||||||
echo "refusing to $verb without --yes." >&2
|
|
||||||
echo " $verb changes live networking — it can cut the path this session" >&2
|
|
||||||
echo " is reaching the estate through. Read 'make vpn check' first." >&2
|
|
||||||
exit 1; }
|
|
||||||
echo "refusing to $verb: not implemented. Bringing a tunnel up or down is" >&2
|
|
||||||
echo " the host's business, and the live one is systemd-managed" >&2
|
|
||||||
echo " (wg-quick@wg0). berth describes and renders; it does not operate." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-list}" in
|
|
||||||
list) list ;;
|
|
||||||
show) shift; show "${1:-}" ;;
|
|
||||||
check) check ;;
|
|
||||||
render) shift; render "${1:-}" ;;
|
|
||||||
keygen) shift; keygen "${1:-}" ;;
|
|
||||||
capture) shift; capture "$@" ;;
|
|
||||||
up|down) v="$1"; shift; refuse "$v" "$@" ;;
|
|
||||||
*) echo "usage: $0 [list|show <ov>|check|render <peer>|keygen <peer>|capture [--as <peer>] [--write]|up --yes|down --yes]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
{
|
|
||||||
"_meta": {
|
|
||||||
"status": "UNVERIFIED — derived from the repos, not from the estate",
|
|
||||||
"why": "ppl/infra/ was written and never applied: no ~/.pulumi, no infra/venv, no stack state, files dated 'mar 6'. The estate was built in the console and the IaC is aspirational. B1's inventory is what replaces these values with observed ones; until it runs, every field here is a CLAIM.",
|
|
||||||
"never_record": "credential values. Resource ids and settings only. nova's gateway secret is deliberately absent from this file even though it is committed in plaintext in ppl/gateway/nginx/conf.d/nova.conf — see services[].raw.",
|
|
||||||
"sources": [
|
|
||||||
"ppl/infra/__main__.py",
|
|
||||||
"ppl/ctrl/dns.sh",
|
|
||||||
"ppl/ctrl/certs.sh",
|
|
||||||
"ppl/gateway/docker-compose.yml",
|
|
||||||
"ppl/gateway/nginx/conf.d/",
|
|
||||||
"ppl/local/Caddyfile"
|
|
||||||
],
|
|
||||||
"placement": {
|
|
||||||
"box": "a container on the estate's own docker network — upstream is the container name",
|
|
||||||
"local": "a rig cluster on a peer, reached over the overlay — upstream is that peer's address",
|
|
||||||
"instance": "a dedicated cloud instance on the overlay — same rendering as `local`",
|
|
||||||
"hosted": "a managed endpoint. Declared so moving to one is a one-line change; unused.",
|
|
||||||
"_why": "A service says WHERE it runs. How it is reached follows from that, and the three properties of a static-upstream vhost — upstream{}, no resolver, no set $var — are one decision rather than three."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain": "mcrn.ar",
|
|
||||||
"local_domain": "local.ar",
|
|
||||||
"host": "mcrn",
|
|
||||||
"host_admin": "mcrn-admin",
|
|
||||||
"instance": {
|
|
||||||
"type": "t3.small",
|
|
||||||
"disk_gb": 30,
|
|
||||||
"disk_type": "gp3",
|
|
||||||
"image": "debian-12",
|
|
||||||
"user": "mariano"
|
|
||||||
},
|
|
||||||
"firewall": [
|
|
||||||
{
|
|
||||||
"port": 22,
|
|
||||||
"proto": "tcp",
|
|
||||||
"desc": "SSH"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"port": 80,
|
|
||||||
"proto": "tcp",
|
|
||||||
"desc": "HTTP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"port": 443,
|
|
||||||
"proto": "tcp",
|
|
||||||
"desc": "HTTPS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"port": 3022,
|
|
||||||
"proto": "tcp",
|
|
||||||
"desc": "Gitea SSH",
|
|
||||||
"note": "compose maps 3022:22 but GITEA__server__SSH_PORT=22, so gitea advertises :22 in clone URLs while listening on :3022. B1 confirms which is real."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"port": 51820,
|
|
||||||
"proto": "udp",
|
|
||||||
"desc": "WireGuard",
|
|
||||||
"note": "ABSENT from ppl/infra/__main__.py's four rules — but the tunnel is live (ping 10.8.0.1 succeeds), so the real security group must already allow it. The code therefore does not describe the estate. Confirm in V1."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"network": {
|
|
||||||
"docker_network": "gateway",
|
|
||||||
"docker_network_note": "A fixed, externally-joinable bridge name. Every unrelated app stack on the box joins it so nginx can resolve them by container name. This is why the gateway compose declares 8 services while nginx routes 20+ hostnames.",
|
|
||||||
"wireguard_moved": "superseded by the top-level `vpn` block"
|
|
||||||
},
|
|
||||||
"vpn": {
|
|
||||||
"_status": "CAPTURED 2026-09-14 — public keys, allowed-ips and keepalive read from `wg show`. Roaming endpoints deliberately not recorded.",
|
|
||||||
"_never_record": "private keys. `wg show` prints 'private key: (hidden)' and is the safe capture command. `wg showconf` dumps PrivateKey= in clear — never use it.",
|
|
||||||
"overlays": {
|
|
||||||
"estate": {
|
|
||||||
"purpose": "Connects the estate's machines across clouds without a shared VPC, and carries everything that does not need to be publicly reachable.",
|
|
||||||
"subnet": "10.8.0.0/24",
|
|
||||||
"listen_port": 51820,
|
|
||||||
"peers": {
|
|
||||||
"box": {
|
|
||||||
"address": "10.8.0.1",
|
|
||||||
"role": "hub",
|
|
||||||
"note": "mcrn.ar. Has a public IP, so it is the peer others dial. Carries the registry (:5000) and woodpecker's gRPC (:9000), both bound to this address and therefore overlay-only.",
|
|
||||||
"endpoint": "3.23.204.197:51820",
|
|
||||||
"public_key": "zVYCmi3xucuX7k/aDhrOUPyN4GRk96ffSDD6dUFQjh4=",
|
|
||||||
"allowed_ips": "10.8.0.0/24",
|
|
||||||
"keepalive": 25,
|
|
||||||
"listen_port": 51820
|
|
||||||
},
|
|
||||||
"nrft": {
|
|
||||||
"address": "10.8.0.2",
|
|
||||||
"role": "roaming",
|
|
||||||
"note": "The dev box. Behind NAT, so it must initiate and needs PersistentKeepalive. Verified: wg0 UP at 10.8.0.2/24, ping 10.8.0.1 0% loss at 153ms.",
|
|
||||||
"endpoint": null,
|
|
||||||
"public_key": "zlIBGs4y5rt6uVdmFBasHpafht6ErxG+R3ySCg5rh3s=",
|
|
||||||
"allowed_ips": "10.8.0.2/32, 192.168.1.0/24",
|
|
||||||
"keepalive": null,
|
|
||||||
"listen_port": 36145
|
|
||||||
},
|
|
||||||
"work": {
|
|
||||||
"address": "10.8.0.3",
|
|
||||||
"role": "roaming",
|
|
||||||
"note": "A work computer, granted access when it was needed. Identified by the user at capture time, 2026-09-14 — it was NOT in the description before, and the wire is where it was found. No handshake and no transfer have ever been recorded for it, so it is a standing grant rather than a live peer: it can connect, and never has. Whether to keep or revoke it is the host's call.",
|
|
||||||
"endpoint": null,
|
|
||||||
"public_key": "ruSZwKt/p60GVsTLSAhcKBIXKkSZsf0gWSmSH1+UgE0=",
|
|
||||||
"allowed_ips": "10.8.0.3/32",
|
|
||||||
"keepalive": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"databases": [
|
|
||||||
"gitea",
|
|
||||||
"woodpecker",
|
|
||||||
"umami"
|
|
||||||
],
|
|
||||||
"certs": {
|
|
||||||
"issued": [
|
|
||||||
"mcrn.ar",
|
|
||||||
"*.mcrn.ar",
|
|
||||||
"*.spr.mcrn.ar"
|
|
||||||
],
|
|
||||||
"issued_source": "ppl/ctrl/certs.sh:92 — the -d flags passed to certbot",
|
|
||||||
"note": "What the cert ACTUALLY covers. estate_sans() derives what the services NEED. check.sh compares the two; the difference is the finding, not a restatement."
|
|
||||||
},
|
|
||||||
"services": [
|
|
||||||
{
|
|
||||||
"name": "gitea",
|
|
||||||
"host": "git",
|
|
||||||
"upstream": "gitea:3000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "woodpecker",
|
|
||||||
"host": "ci",
|
|
||||||
"upstream": "woodpecker-server:8000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "registry",
|
|
||||||
"host": "registry",
|
|
||||||
"upstream": "registry:5000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "umami",
|
|
||||||
"host": "analytics",
|
|
||||||
"upstream": "umami:3000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "docserve",
|
|
||||||
"host": "docs",
|
|
||||||
"upstream": "docserve:8020",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ghost",
|
|
||||||
"host": "notes",
|
|
||||||
"upstream": "ghost:2368",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "deskmeter",
|
|
||||||
"host": "deskmeter",
|
|
||||||
"upstream": "dmweb:10000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"local_port": 10000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sysmonstm",
|
|
||||||
"host": "sysmonstm",
|
|
||||||
"upstream": "sysmonstm-edge:8080",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"local_port": 8020
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "malvalava",
|
|
||||||
"host": "malvalava",
|
|
||||||
"upstream": "mlvclean-frontend:80",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"local_port": 30090
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "soleprint",
|
|
||||||
"host": "soleprint",
|
|
||||||
"upstream": "soleprint:8000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"local_port": 12000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dlt",
|
|
||||||
"host": "dlt.spr",
|
|
||||||
"upstream": "dlt_spr:8000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sample",
|
|
||||||
"host": "sample.spr",
|
|
||||||
"upstream": "sample_spr:8000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mariano",
|
|
||||||
"host": "mariano",
|
|
||||||
"kind": "static",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rigui",
|
|
||||||
"host": "rig",
|
|
||||||
"kind": "static",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"local_port": 20310
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "unt",
|
|
||||||
"host": "unt",
|
|
||||||
"targets": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"local_port": 8040
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mpr",
|
|
||||||
"host": "mpr",
|
|
||||||
"targets": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"local_port": 30080
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nvi",
|
|
||||||
"host": "nvi",
|
|
||||||
"targets": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"local_port": 8060
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "eth",
|
|
||||||
"host": "eth",
|
|
||||||
"targets": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"local_port": 8050
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "amar",
|
|
||||||
"host": "amar",
|
|
||||||
"targets": [
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"local_port": 8030
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nova",
|
|
||||||
"host": "nova",
|
|
||||||
"upstream": "nova-ui:80",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"raw": true,
|
|
||||||
"raw_why": "Gated on an X-Gateway-Secret header whose value is committed in plaintext. The value is NOT recorded here. Worse: stellarair.conf proxies to the SAME nova-ui:80 upstream WITHOUT the check, so the gate is bypassable by hostname. Stays hand-written until that is decided."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stellarair",
|
|
||||||
"host": "stellarair",
|
|
||||||
"upstream": "nova-ui:80",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"raw": true,
|
|
||||||
"raw_why": "See nova. Same upstream, no header gate."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "langfuse",
|
|
||||||
"host": "langfuse",
|
|
||||||
"local_host": "lng",
|
|
||||||
"placement": "local",
|
|
||||||
"peer": "nrft",
|
|
||||||
"port": 3000,
|
|
||||||
"targets": [
|
|
||||||
"aws",
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"local_port": 3000,
|
|
||||||
"note": "One service, one socket, two names. It was two entries with one flagged `raw`; placement is what made the exception expressible, so it is generated now."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "legacy",
|
|
||||||
"host": "*.soleprint",
|
|
||||||
"upstream": "soleprint:8000",
|
|
||||||
"targets": [
|
|
||||||
"aws"
|
|
||||||
],
|
|
||||||
"raw": true,
|
|
||||||
"raw_why": "A regex server_name with a named capture plus sub_filter injection — not expressible as a template. ALSO BROKEN: its /api/, /admin/, /static/ and / blocks proxy to 127.0.0.1, i.e. inside the nginx container where nothing listens, so every legacy room 502s. Only /wrapper/ uses the correct container-name form."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
500
build.py
500
build.py
@@ -23,12 +23,9 @@ Generated structure for managed rooms:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import importlib.util
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -51,59 +48,6 @@ def ensure_dir(path: Path):
|
|||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def _rmtree_resilient(path: Path):
|
|
||||||
"""Remove path tree, tolerating root-owned files written by containers.
|
|
||||||
|
|
||||||
Docker containers that mount gen/ as a volume sometimes write files as
|
|
||||||
root (e.g. __pycache__). A plain shutil.rmtree then fails with EACCES.
|
|
||||||
We first try shutil.rmtree; if that hits a PermissionError we fall back
|
|
||||||
to deleting the offending files from inside an ephemeral alpine container.
|
|
||||||
"""
|
|
||||||
def _chmod_and_retry(func, target, exc_info):
|
|
||||||
try:
|
|
||||||
Path(target).chmod(stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR)
|
|
||||||
func(target)
|
|
||||||
except Exception:
|
|
||||||
raise
|
|
||||||
|
|
||||||
try:
|
|
||||||
shutil.rmtree(path, onerror=_chmod_and_retry)
|
|
||||||
return
|
|
||||||
except PermissionError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log.info(" (falling back to docker-based cleanup)")
|
|
||||||
subprocess.run(
|
|
||||||
["docker", "run", "--rm", "-v", f"{path.parent}:/work",
|
|
||||||
"alpine:3", "sh", "-c", f"rm -rf /work/{path.name}"],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Never swept into a built room, wherever they appear in a source tree.
|
|
||||||
#
|
|
||||||
# This is a SECURITY boundary, not tidiness. gen/<room>/ is the docker build
|
|
||||||
# context, soleprint/Dockerfile is `COPY . .`, and there is no .dockerignore —
|
|
||||||
# so anything that reaches gen/ reaches an image layer, and registry.mcrn.ar is
|
|
||||||
# public-read. That is how station/tools/tester/.env, gitignored since the last
|
|
||||||
# incident, still ended up baked into soleprint_localtest-soleprint:latest with
|
|
||||||
# its API key intact. .gitignore does not bind shutil.
|
|
||||||
#
|
|
||||||
# Applied to bulk directory copies only. A caller naming a single file is making
|
|
||||||
# an explicit request (cfg/<room>/.env.example is the one that matters) and is
|
|
||||||
# left alone.
|
|
||||||
ALWAYS_IGNORE = {".git", "__pycache__", "node_modules", ".venv", "venv", ".env"}
|
|
||||||
ALWAYS_IGNORE_SUFFIXES = (".pyc", ".pyo")
|
|
||||||
|
|
||||||
|
|
||||||
def is_ignored(name: str) -> bool:
|
|
||||||
return name in ALWAYS_IGNORE or name.endswith(ALWAYS_IGNORE_SUFFIXES)
|
|
||||||
|
|
||||||
|
|
||||||
def _copytree_ignore(directory, files):
|
|
||||||
return {f for f in files if is_ignored(f)}
|
|
||||||
|
|
||||||
|
|
||||||
def copy_path(source: Path, target: Path, quiet: bool = False):
|
def copy_path(source: Path, target: Path, quiet: bool = False):
|
||||||
"""Copy file or directory, resolving symlinks."""
|
"""Copy file or directory, resolving symlinks."""
|
||||||
if target.is_symlink():
|
if target.is_symlink():
|
||||||
@@ -115,7 +59,7 @@ def copy_path(source: Path, target: Path, quiet: bool = False):
|
|||||||
target.unlink()
|
target.unlink()
|
||||||
|
|
||||||
if source.is_dir():
|
if source.is_dir():
|
||||||
shutil.copytree(source, target, symlinks=False, ignore=_copytree_ignore)
|
shutil.copytree(source, target, symlinks=False)
|
||||||
if not quiet:
|
if not quiet:
|
||||||
log.info(f" {target.name}/")
|
log.info(f" {target.name}/")
|
||||||
else:
|
else:
|
||||||
@@ -135,8 +79,6 @@ def merge_into(source: Path, target: Path):
|
|||||||
for item in source.rglob("*"):
|
for item in source.rglob("*"):
|
||||||
if item.is_file():
|
if item.is_file():
|
||||||
rel = item.relative_to(source)
|
rel = item.relative_to(source)
|
||||||
if any(is_ignored(part) for part in rel.parts):
|
|
||||||
continue
|
|
||||||
dest = target / rel
|
dest = target / rel
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy2(item, dest)
|
shutil.copy2(item, dest)
|
||||||
@@ -220,11 +162,9 @@ def build_managed(output_dir: Path, cfg_name: str, config: dict):
|
|||||||
|
|
||||||
log.info(f"Building managed ({managed_name})...")
|
log.info(f"Building managed ({managed_name})...")
|
||||||
|
|
||||||
# Copy repos (relative paths resolve from SPR_ROOT)
|
# Copy repos
|
||||||
for repo_name, repo_path in repos.items():
|
for repo_name, repo_path in repos.items():
|
||||||
source = Path(repo_path)
|
source = Path(repo_path)
|
||||||
if not source.is_absolute():
|
|
||||||
source = SPR_ROOT / source
|
|
||||||
target = managed_dir / repo_name
|
target = managed_dir / repo_name
|
||||||
if copy_repo(source, target):
|
if copy_repo(source, target):
|
||||||
log.info(f" {repo_name}/")
|
log.info(f" {repo_name}/")
|
||||||
@@ -331,374 +271,16 @@ def copy_cfg(output_dir: Path, room: str):
|
|||||||
# Now in cfg/<room>/soleprint/
|
# Now in cfg/<room>/soleprint/
|
||||||
room_soleprint = room_cfg / "soleprint"
|
room_soleprint = room_cfg / "soleprint"
|
||||||
if room_soleprint.exists():
|
if room_soleprint.exists():
|
||||||
systems = {"artery", "atlas", "station"}
|
|
||||||
for item in room_soleprint.iterdir():
|
for item in room_soleprint.iterdir():
|
||||||
if item.name in systems:
|
if item.is_file():
|
||||||
# Merge system dirs into already-copied framework code
|
|
||||||
log.info(f" Merging {room} {item.name}...")
|
|
||||||
merge_into(item, output_dir / item.name)
|
|
||||||
elif item.is_file():
|
|
||||||
copy_path(item, output_dir / item.name)
|
|
||||||
elif item.is_dir():
|
|
||||||
# Copy non-system dirs as-is (nginx/, etc.)
|
|
||||||
copy_path(item, output_dir / item.name)
|
copy_path(item, output_dir / item.name)
|
||||||
|
|
||||||
|
# Merge room-specific system configs from soleprint subfolder
|
||||||
def load_cabinets(room: str) -> list[dict]:
|
for system in ["artery", "atlas", "station"]:
|
||||||
"""The dependency containers a room asked for, in the order it listed them.
|
room_system = room_soleprint / system
|
||||||
|
if room_system.exists():
|
||||||
Read from cfg/<room>/data/cabinets.json — the same shape and the same place
|
log.info(f" Merging {room} {system}...")
|
||||||
as its sibling data/*.json files, so nothing new has to know about it.
|
merge_into(room_system, output_dir / system)
|
||||||
Entries are {"name": "postgres"} and may carry an "env" override.
|
|
||||||
"""
|
|
||||||
path = SPR_ROOT / "cfg" / room / "data" / "cabinets.json"
|
|
||||||
if not path.exists():
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
entries = json.loads(path.read_text())
|
|
||||||
except ValueError as e:
|
|
||||||
log.warning(f" cabinets.json is not valid JSON, ignoring: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
out = []
|
|
||||||
for entry in entries if isinstance(entries, list) else []:
|
|
||||||
if isinstance(entry, str):
|
|
||||||
entry = {"name": entry}
|
|
||||||
if isinstance(entry, dict) and entry.get("name"):
|
|
||||||
out.append(entry)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_cabinets(requested: list[dict]) -> list[dict]:
|
|
||||||
"""Expand each request into its definition, pulling in what it depends on.
|
|
||||||
|
|
||||||
Airflow without postgres is a container that exits on boot, so a cabinet's
|
|
||||||
depends_on is added for you rather than left as something to remember.
|
|
||||||
"""
|
|
||||||
cabinets_dir = SPR_ROOT / "soleprint" / "station" / "cabinets"
|
|
||||||
resolved: dict[str, dict] = {}
|
|
||||||
|
|
||||||
def add(name: str, overrides: dict) -> None:
|
|
||||||
if name in resolved:
|
|
||||||
# Already pulled in as somebody's dependency. The room asking for it
|
|
||||||
# by name is the more specific statement, so its env still applies —
|
|
||||||
# otherwise declaring airflow before postgres would silently drop
|
|
||||||
# postgres's settings.
|
|
||||||
if overrides.get("env"):
|
|
||||||
resolved[name]["env"] = {
|
|
||||||
**resolved[name].get("env", {}),
|
|
||||||
**overrides["env"],
|
|
||||||
}
|
|
||||||
return
|
|
||||||
definition_path = cabinets_dir / name / "cabinet.json"
|
|
||||||
if not definition_path.exists():
|
|
||||||
available = sorted(
|
|
||||||
p.name for p in cabinets_dir.iterdir() if p.is_dir()
|
|
||||||
) if cabinets_dir.exists() else []
|
|
||||||
log.warning(f" no such cabinet: {name} (available: {', '.join(available) or 'none'})")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
definition = json.loads(definition_path.read_text())
|
|
||||||
except ValueError as e:
|
|
||||||
log.warning(f" cabinet {name} has invalid cabinet.json: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mark it claimed before recursing, so a dependency cycle terminates.
|
|
||||||
resolved[name] = definition
|
|
||||||
for dependency in definition.get("depends_on", []) or []:
|
|
||||||
add(dependency, {})
|
|
||||||
|
|
||||||
definition["env"] = {**definition.get("env", {}), **overrides.get("env", {})}
|
|
||||||
|
|
||||||
for entry in requested:
|
|
||||||
add(entry["name"], entry)
|
|
||||||
|
|
||||||
# Dependencies first, so compose reads in the order things start.
|
|
||||||
ordered: list[dict] = []
|
|
||||||
seen: set[str] = set()
|
|
||||||
|
|
||||||
def emit(name: str) -> None:
|
|
||||||
if name in seen or name not in resolved:
|
|
||||||
return
|
|
||||||
seen.add(name)
|
|
||||||
for dependency in resolved[name].get("depends_on", []) or []:
|
|
||||||
emit(dependency)
|
|
||||||
ordered.append(resolved[name])
|
|
||||||
|
|
||||||
for name in resolved:
|
|
||||||
emit(name)
|
|
||||||
return ordered
|
|
||||||
|
|
||||||
|
|
||||||
def compose_cabinets(output_dir: Path, room: str):
|
|
||||||
"""Merge the room's cabinets into its docker-compose.yml and .env.example.
|
|
||||||
|
|
||||||
This is the compile step for dependencies: a room declares postgres, and the
|
|
||||||
built instance comes out with postgres in its compose file rather than with
|
|
||||||
instructions for adding it.
|
|
||||||
"""
|
|
||||||
requested = load_cabinets(room)
|
|
||||||
if not requested:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
import yaml
|
|
||||||
except ImportError:
|
|
||||||
log.warning(
|
|
||||||
" cabinets need PyYAML to merge into docker-compose.yml "
|
|
||||||
"(pip install pyyaml) — skipping"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
cabinets = resolve_cabinets(requested)
|
|
||||||
if not cabinets:
|
|
||||||
return
|
|
||||||
|
|
||||||
compose_path = output_dir / "docker-compose.yml"
|
|
||||||
if not compose_path.exists():
|
|
||||||
log.warning(
|
|
||||||
f" no docker-compose.yml in {output_dir.name}, "
|
|
||||||
f"so there is nothing to merge {len(cabinets)} cabinet(s) into"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
original = compose_path.read_text()
|
|
||||||
# A YAML round-trip drops every comment, and the room's compose file leads
|
|
||||||
# with the one that says how to run it. Keep the header block; the rest is
|
|
||||||
# generated anyway.
|
|
||||||
header = []
|
|
||||||
for line in original.splitlines():
|
|
||||||
if line.startswith("#") or not line.strip():
|
|
||||||
header.append(line)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
while header and not header[-1].strip():
|
|
||||||
header.pop()
|
|
||||||
|
|
||||||
compose = yaml.safe_load(original) or {}
|
|
||||||
services = compose.setdefault("services", {})
|
|
||||||
volumes = compose.setdefault("volumes", {}) or {}
|
|
||||||
cabinets_dir = SPR_ROOT / "soleprint" / "station" / "cabinets"
|
|
||||||
|
|
||||||
added, skipped = [], []
|
|
||||||
for cabinet in cabinets:
|
|
||||||
name = cabinet["name"]
|
|
||||||
service_name = cabinet.get("service", name)
|
|
||||||
|
|
||||||
# The room's own compose file is the authority. A room that already
|
|
||||||
# declares `db` has arranged it deliberately, and silently replacing it
|
|
||||||
# would be the worst possible outcome of switching a cabinet on.
|
|
||||||
if service_name in services:
|
|
||||||
skipped.append(service_name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
fragment_path = cabinets_dir / name / "service.yml"
|
|
||||||
if not fragment_path.exists():
|
|
||||||
log.warning(f" cabinet {name} has no service.yml")
|
|
||||||
continue
|
|
||||||
|
|
||||||
fragment = yaml.safe_load(fragment_path.read_text()) or {}
|
|
||||||
for key, value in fragment.items():
|
|
||||||
if key in services:
|
|
||||||
skipped.append(key)
|
|
||||||
continue
|
|
||||||
services[key] = value
|
|
||||||
added.append(key)
|
|
||||||
|
|
||||||
for volume in cabinet.get("volumes", []) or []:
|
|
||||||
volumes.setdefault(volume, None)
|
|
||||||
|
|
||||||
if volumes:
|
|
||||||
compose["volumes"] = volumes
|
|
||||||
|
|
||||||
rendered = yaml.safe_dump(compose, sort_keys=False, default_flow_style=False)
|
|
||||||
banner = f"# Cabinets merged in by build.py: {', '.join(c['name'] for c in cabinets)}.\n"
|
|
||||||
preamble = ("\n".join(header) + "\n" + banner + "\n") if header else banner + "\n"
|
|
||||||
compose_path.write_text(preamble + rendered)
|
|
||||||
|
|
||||||
if added:
|
|
||||||
log.info(f" cabinets: {', '.join(added)}")
|
|
||||||
if skipped:
|
|
||||||
log.info(f" cabinets already declared by the room, left alone: {', '.join(skipped)}")
|
|
||||||
|
|
||||||
_append_cabinet_env(output_dir, cabinets)
|
|
||||||
|
|
||||||
|
|
||||||
def _append_cabinet_env(output_dir: Path, cabinets: list[dict]):
|
|
||||||
"""Add each cabinet's settings to .env.example, without touching .env."""
|
|
||||||
example = output_dir / ".env.example"
|
|
||||||
existing = example.read_text() if example.exists() else ""
|
|
||||||
# Match whole settings, not substrings: `POSTGRES_DB=` appears inside
|
|
||||||
# `MY_POSTGRES_DB=`, and a substring test would decide the setting was
|
|
||||||
# already there and skip it.
|
|
||||||
declared = {
|
|
||||||
line.split("=", 1)[0].strip()
|
|
||||||
for line in existing.splitlines()
|
|
||||||
if "=" in line and not line.lstrip().startswith("#")
|
|
||||||
}
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
for cabinet in cabinets:
|
|
||||||
env = cabinet.get("env", {})
|
|
||||||
if not env:
|
|
||||||
continue
|
|
||||||
block = [f"\n# ── {cabinet.get('title', cabinet['name'])} (cabinet) ──"]
|
|
||||||
for note in cabinet.get("notes", []) or []:
|
|
||||||
block.append(f"# {note}")
|
|
||||||
wrote = False
|
|
||||||
for key, value in env.items():
|
|
||||||
if key in declared:
|
|
||||||
continue
|
|
||||||
block.append(f"{key}={value}")
|
|
||||||
declared.add(key)
|
|
||||||
wrote = True
|
|
||||||
if wrote:
|
|
||||||
lines.extend(block)
|
|
||||||
|
|
||||||
if lines:
|
|
||||||
example.write_text(existing.rstrip("\n") + "\n" + "\n".join(lines) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def load_plexuses(room: str) -> list[dict]:
|
|
||||||
"""The plexuses a room asked for. Same shape as its sibling data/*.json."""
|
|
||||||
path = SPR_ROOT / "cfg" / room / "data" / "plexuses.json"
|
|
||||||
if not path.exists():
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
raw = json.loads(path.read_text())
|
|
||||||
except ValueError as e:
|
|
||||||
log.warning(f" plexuses.json is not valid JSON, ignoring: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
entries = raw.get("items", raw) if isinstance(raw, dict) else raw
|
|
||||||
out = []
|
|
||||||
for entry in entries if isinstance(entries, list) else []:
|
|
||||||
if isinstance(entry, str):
|
|
||||||
entry = {"name": entry}
|
|
||||||
if isinstance(entry, dict) and entry.get("name"):
|
|
||||||
out.append(entry)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _theme_css(theme: str) -> str:
|
|
||||||
"""The token contract plus one theme, flattened for inlining.
|
|
||||||
|
|
||||||
Only the named theme ships alongside the others it can switch to, because
|
|
||||||
the export has to work with no server: there is no /theme.css to fetch.
|
|
||||||
"""
|
|
||||||
theme_dir = SPR_ROOT / "soleprint" / "common" / "theme"
|
|
||||||
parts = []
|
|
||||||
tokens = theme_dir / "tokens.css"
|
|
||||||
if tokens.exists():
|
|
||||||
parts.append(tokens.read_text())
|
|
||||||
# Every theme, so the switcher in the page has something to switch to.
|
|
||||||
for sheet in sorted((theme_dir / "themes").glob("*.css")):
|
|
||||||
parts.append(sheet.read_text())
|
|
||||||
return "\n".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def _inline_svg(name: str, theme: str) -> str:
|
|
||||||
"""A rendered graph, stripped of its XML prolog so it can sit in HTML.
|
|
||||||
|
|
||||||
Inlined rather than <img>-linked so the page's CSS can recolour it when the
|
|
||||||
theme switches — graphviz writes class="node accent" into the SVG, and CSS
|
|
||||||
outranks the presentation attributes it bakes in.
|
|
||||||
"""
|
|
||||||
graphs = SPR_ROOT / "docs" / "graphs"
|
|
||||||
for candidate in (graphs / f"{name}.{theme}.svg", graphs / f"{name}.svg"):
|
|
||||||
if candidate.exists():
|
|
||||||
svg = candidate.read_text()
|
|
||||||
start = svg.find("<svg")
|
|
||||||
return svg[start:] if start >= 0 else svg
|
|
||||||
log.warning(f" no rendered graph '{name}' — run docs/graphs/render.sh")
|
|
||||||
return "<p>diagram not rendered</p>"
|
|
||||||
|
|
||||||
|
|
||||||
def build_plexuses(output_dir: Path, room: str):
|
|
||||||
"""Export each plexus the room declared to a single self-contained file.
|
|
||||||
|
|
||||||
A plexus is exported, not served. The output is one index.html carrying its
|
|
||||||
theme, its data and its diagram, so it survives a locked-down machine, a zip
|
|
||||||
attachment and a double-click — which is the whole point of the format.
|
|
||||||
"""
|
|
||||||
requested = load_plexuses(room)
|
|
||||||
if not requested:
|
|
||||||
return
|
|
||||||
|
|
||||||
source_root = SPR_ROOT / "soleprint" / "artery" / "plexuses"
|
|
||||||
built = []
|
|
||||||
|
|
||||||
for entry in requested:
|
|
||||||
name = entry["name"]
|
|
||||||
source = source_root / name
|
|
||||||
manifest_path = source / "plexus.json"
|
|
||||||
if not manifest_path.exists():
|
|
||||||
available = sorted(
|
|
||||||
p.name for p in source_root.iterdir() if p.is_dir()
|
|
||||||
) if source_root.exists() else []
|
|
||||||
log.warning(
|
|
||||||
f" no such plexus: {name} (available: {', '.join(available) or 'none'})"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
manifest = json.loads(manifest_path.read_text())
|
|
||||||
except ValueError as e:
|
|
||||||
log.warning(f" plexus {name} has invalid plexus.json: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# The room may override anything the plexus declares — theme first.
|
|
||||||
manifest.update({k: v for k, v in entry.items() if k != "name"})
|
|
||||||
|
|
||||||
template_path = source / "app" / "index.html"
|
|
||||||
if not template_path.exists():
|
|
||||||
log.warning(f" plexus {name} has no app/index.html")
|
|
||||||
continue
|
|
||||||
|
|
||||||
theme = manifest.get("theme", "soleprint")
|
|
||||||
data = {k: v for k, v in manifest.items() if not k.startswith("_")}
|
|
||||||
|
|
||||||
# A plexus may ship a showcase.py exposing collect(): anything it returns
|
|
||||||
# is merged into the page's data. The bundle uses it to run the real
|
|
||||||
# tools over the real fixtures at build time, so what the page shows
|
|
||||||
# cannot drift from what the tools do.
|
|
||||||
collector = source / "showcase.py"
|
|
||||||
if collector.exists():
|
|
||||||
try:
|
|
||||||
spec = importlib.util.spec_from_file_location(
|
|
||||||
f"plexus_{name}_showcase", collector
|
|
||||||
)
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
data["showcase"] = module.collect()
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f" {name}: showcase.py failed ({type(e).__name__}: {e})")
|
|
||||||
|
|
||||||
page = template_path.read_text()
|
|
||||||
for token, value in (
|
|
||||||
("%%TITLE%%", manifest.get("title", name)),
|
|
||||||
("%%DESCRIPTION%%", manifest.get("description", "")),
|
|
||||||
("%%DEFAULT_THEME%%", theme),
|
|
||||||
("%%BUILT%%", f"{room} · built by soleprint build.py"),
|
|
||||||
("%%THEME_CSS%%", _theme_css(theme)),
|
|
||||||
("%%GRAPH%%", _inline_svg(manifest.get("graph", "system_overview"), theme)),
|
|
||||||
("%%BUNDLE%%", json.dumps(data, indent=2)),
|
|
||||||
):
|
|
||||||
page = page.replace(token, value)
|
|
||||||
|
|
||||||
target = output_dir / "plexuses" / name
|
|
||||||
ensure_dir(target)
|
|
||||||
(target / "index.html").write_text(page)
|
|
||||||
|
|
||||||
# Anything else in app/ rides along, for a plexus that outgrows one file.
|
|
||||||
for extra in (source / "app").iterdir():
|
|
||||||
if extra.name != "index.html":
|
|
||||||
copy_path(extra, target / extra.name, quiet=True)
|
|
||||||
|
|
||||||
built.append(f"{name} ({theme})")
|
|
||||||
|
|
||||||
if built:
|
|
||||||
log.info(f" plexuses: {', '.join(built)}")
|
|
||||||
|
|
||||||
|
|
||||||
def build_soleprint(output_dir: Path, room: str):
|
def build_soleprint(output_dir: Path, room: str):
|
||||||
@@ -713,7 +295,6 @@ def build_soleprint(output_dir: Path, room: str):
|
|||||||
"index.html",
|
"index.html",
|
||||||
"requirements.txt",
|
"requirements.txt",
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
".dockerignore",
|
|
||||||
]:
|
]:
|
||||||
if (soleprint / name).exists():
|
if (soleprint / name).exists():
|
||||||
copy_path(soleprint / name, output_dir / name)
|
copy_path(soleprint / name, output_dir / name)
|
||||||
@@ -733,16 +314,6 @@ def build_soleprint(output_dir: Path, room: str):
|
|||||||
# Room config (includes merging room-specific artery/atlas/station)
|
# Room config (includes merging room-specific artery/atlas/station)
|
||||||
copy_cfg(output_dir, room)
|
copy_cfg(output_dir, room)
|
||||||
|
|
||||||
# Dependency containers the room asked for, merged into its compose file.
|
|
||||||
# After copy_cfg, because the compose file being merged into is the room's.
|
|
||||||
log.info("Composing cabinets...")
|
|
||||||
compose_cabinets(output_dir, room)
|
|
||||||
|
|
||||||
# Plexuses are exported rather than served, so this is a compile step like
|
|
||||||
# the cabinet merge above — not something run.py does at request time.
|
|
||||||
log.info("Exporting plexuses...")
|
|
||||||
build_plexuses(output_dir, room)
|
|
||||||
|
|
||||||
# Generate models
|
# Generate models
|
||||||
log.info("Generating models...")
|
log.info("Generating models...")
|
||||||
if not generate_models(output_dir, room):
|
if not generate_models(output_dir, room):
|
||||||
@@ -760,7 +331,7 @@ def build(output_dir: Path, cfg_name: str | None = None, clean: bool = True):
|
|||||||
# Clean output directory first
|
# Clean output directory first
|
||||||
if clean and output_dir.exists():
|
if clean and output_dir.exists():
|
||||||
log.info(f"Cleaning {output_dir}...")
|
log.info(f"Cleaning {output_dir}...")
|
||||||
_rmtree_resilient(output_dir)
|
shutil.rmtree(output_dir)
|
||||||
|
|
||||||
ensure_dir(output_dir)
|
ensure_dir(output_dir)
|
||||||
|
|
||||||
@@ -777,15 +348,6 @@ def build(output_dir: Path, cfg_name: str | None = None, clean: bool = True):
|
|||||||
# Standalone: everything in output_dir
|
# Standalone: everything in output_dir
|
||||||
build_soleprint(output_dir, room)
|
build_soleprint(output_dir, room)
|
||||||
|
|
||||||
# Layer 7 (optional): render kind-cluster manifests
|
|
||||||
try:
|
|
||||||
from soleprint.ctrl.k8s import render_k8s
|
|
||||||
from soleprint.ctrl.k8s.render import k8s_enabled
|
|
||||||
if k8s_enabled(config):
|
|
||||||
render_k8s(room=room, config=config, gen_dir=output_dir)
|
|
||||||
except ImportError as e:
|
|
||||||
log.warning(f"k8s rendering unavailable: {e}")
|
|
||||||
|
|
||||||
log.info(f"\n✓ Built: {output_dir}")
|
log.info(f"\n✓ Built: {output_dir}")
|
||||||
|
|
||||||
|
|
||||||
@@ -799,33 +361,6 @@ def build_models_only():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def build_plexuses_only(room: str):
|
|
||||||
"""Compile just the plexus UIs, without rebuilding the room around them.
|
|
||||||
|
|
||||||
The equivalent of `vite build` for this repo: the iteration loop when you
|
|
||||||
are working on the UI itself is edit, compile, reopen the file — and a full
|
|
||||||
room build to see a CSS change is a slow way to do that.
|
|
||||||
"""
|
|
||||||
output_dir = SPR_ROOT / "gen" / room
|
|
||||||
if not output_dir.exists():
|
|
||||||
log.error(f"Room '{room}' is not built — run: python build.py --cfg {room}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
log.info(f"Compiling plexus UIs for {room}...")
|
|
||||||
build_plexuses(output_dir, room)
|
|
||||||
|
|
||||||
built = sorted((output_dir / "plexuses").glob("*/index.html"))
|
|
||||||
if not built:
|
|
||||||
log.warning(
|
|
||||||
f" nothing compiled — does cfg/{room}/data/plexuses.json list one?"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
for page in built:
|
|
||||||
log.info(f" {page.relative_to(SPR_ROOT)} ({page.stat().st_size // 1024} KB)")
|
|
||||||
log.info("\n✓ Open directly — no server needed:")
|
|
||||||
log.info(f" xdg-open {built[0]}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Soleprint Build Tool")
|
parser = argparse.ArgumentParser(description="Soleprint Build Tool")
|
||||||
|
|
||||||
@@ -833,26 +368,15 @@ def main():
|
|||||||
parser.add_argument("--cfg", "-c", type=str, help="Room config name")
|
parser.add_argument("--cfg", "-c", type=str, help="Room config name")
|
||||||
parser.add_argument("--all", action="store_true", help="Build all rooms")
|
parser.add_argument("--all", action="store_true", help="Build all rooms")
|
||||||
parser.add_argument("--models", action="store_true", help="Only regenerate models")
|
parser.add_argument("--models", action="store_true", help="Only regenerate models")
|
||||||
parser.add_argument(
|
|
||||||
"--plexuses",
|
|
||||||
action="store_true",
|
|
||||||
help="Only compile the plexus UIs into an already-built room",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.plexuses:
|
if args.models:
|
||||||
build_plexuses_only(args.cfg or "standalone")
|
|
||||||
elif args.models:
|
|
||||||
build_models_only()
|
build_models_only()
|
||||||
elif args.all:
|
elif args.all:
|
||||||
build(SPR_ROOT / "gen" / "standalone", None)
|
build(SPR_ROOT / "gen" / "standalone", None)
|
||||||
for room in (SPR_ROOT / "cfg").iterdir():
|
for room in (SPR_ROOT / "cfg").iterdir():
|
||||||
# cfg/ is itself a git repo and rooms may carry dot-dirs — skip them,
|
if room.is_dir() and room.name not in ("__pycache__", "standalone"):
|
||||||
# or --all tries to build ".git" as a room.
|
|
||||||
if room.name.startswith(".") or room.name == "__pycache__":
|
|
||||||
continue
|
|
||||||
if room.is_dir() and room.name != "standalone":
|
|
||||||
build(SPR_ROOT / "gen" / room.name, room.name)
|
build(SPR_ROOT / "gen" / room.name, room.name)
|
||||||
else:
|
else:
|
||||||
if args.output:
|
if args.output:
|
||||||
|
|||||||
10
cfg/.gitignore
vendored
10
cfg/.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# Environment files with credentials (use .env.example as template)
|
# Environment files with credentials (use .env.example as template)
|
||||||
**/.env
|
**/.env
|
||||||
|
!sample/**/.env
|
||||||
|
|
||||||
# Database dumps (sensitive data)
|
# Database dumps (sensitive data)
|
||||||
**/dumps/*.sql
|
**/dumps/*.sql
|
||||||
@@ -9,9 +10,6 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
|
||||||
standalone
|
# These are kept in main soleprint repo as templates
|
||||||
sample
|
standalone/
|
||||||
|
sample/
|
||||||
|
|
||||||
# standalone/ and sample/ are kept in the main soleprint repo as templates.
|
|
||||||
# Other rooms (amar/, dlt/, etc.) are listed in the repo-root .gitignore.
|
|
||||||
|
|||||||
104
cfg/amar/.env
Normal file
104
cfg/amar/.env
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# AMAR - Local Development Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DEPLOYMENT
|
||||||
|
# =============================================================================
|
||||||
|
DEPLOYMENT_NAME=amar
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# NETWORK (shared with soleprint)
|
||||||
|
# =============================================================================
|
||||||
|
NETWORK_NAME=soleprint_network
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PATHS (local code locations)
|
||||||
|
# =============================================================================
|
||||||
|
BACKEND_PATH=/home/mariano/wdir/ama/amar_django_back
|
||||||
|
FRONTEND_PATH=/home/mariano/wdir/ama/amar_frontend
|
||||||
|
DOCKERFILE_BACKEND=/home/mariano/wdir/spr/cfg/amar/Dockerfile.backend
|
||||||
|
DOCKERFILE_FRONTEND=/home/mariano/wdir/spr/cfg/amar/Dockerfile.frontend
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DATABASE
|
||||||
|
# =============================================================================
|
||||||
|
DB_DUMP=dev.sql
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PORTS
|
||||||
|
# =============================================================================
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BACKEND SERVER (Uvicorn)
|
||||||
|
# =============================================================================
|
||||||
|
BACKEND_WORKERS=1
|
||||||
|
BACKEND_RELOAD=--reload
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
POSTGRES_DB=amarback
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=localdev123
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DJANGO
|
||||||
|
# =============================================================================
|
||||||
|
SECRET_KEY=local-dev-key
|
||||||
|
DEBUG=True
|
||||||
|
DJANGO_ENV=development
|
||||||
|
ALLOWED_HOSTS=*
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CORS
|
||||||
|
# =============================================================================
|
||||||
|
CORS_ALLOW_ALL=true
|
||||||
|
CORS_ALLOWED_ORIGINS=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GOOGLE SERVICES
|
||||||
|
# =============================================================================
|
||||||
|
SUBJECT_CALENDAR=
|
||||||
|
SHEET_ID=
|
||||||
|
RANGE_NAME=
|
||||||
|
GOOGLE_MAPS_API_KEY=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ANALYTICS
|
||||||
|
# =============================================================================
|
||||||
|
GA4_MEASUREMENT_ID=
|
||||||
|
AMPLITUDE_API_KEY=
|
||||||
|
HOTJAR_API_KEY=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MERCADO PAGO
|
||||||
|
# =============================================================================
|
||||||
|
ACCESS_TOKEN_MERCADO_PAGO=
|
||||||
|
MP_PLATFORM_ACCESS_TOKEN=
|
||||||
|
USER_ID=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WEB PUSH
|
||||||
|
# =============================================================================
|
||||||
|
WEBPUSH_VAPID_PUBLIC_KEY=
|
||||||
|
WEBPUSH_VAPID_PRIVATE_KEY=
|
||||||
|
WEBPUSH_VAPID_ADMIN_EMAIL=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# INIT
|
||||||
|
# =============================================================================
|
||||||
|
USER_PASSWORD=initial_admin_password
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FRONTEND
|
||||||
|
# =============================================================================
|
||||||
|
NEXT_PUBLIC_APP_API_URL_BACKOFFICE=
|
||||||
|
NEXT_PUBLIC_APP_API_URL_STAGE=
|
||||||
|
NEXT_PUBLIC_IS_STAGE=false
|
||||||
|
NEXT_PUBLIC_FB_PIXEL_ID=
|
||||||
|
NEXT_PUBLIC_TAG_MANAGER=
|
||||||
|
NEXT_PUBLIC_WHATSAPP_CONTACT=
|
||||||
|
NEXT_PUBLIC_API_KEY=
|
||||||
|
NEXT_PUBLIC_AMPLITUDE_API_KEY=
|
||||||
|
NEXT_PUBLIC_GMAPS_API_KEY=
|
||||||
114
cfg/amar/.env.example
Normal file
114
cfg/amar/.env.example
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# AMAR Room - Configuration Template
|
||||||
|
# =============================================================================
|
||||||
|
# Copy this to .env and fill in values for your environment.
|
||||||
|
# Uses absolute paths to plug soleprint to existing Dockerfiles.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DEPLOYMENT
|
||||||
|
# =============================================================================
|
||||||
|
# Container name prefix (e.g., amar_backend, amar_db)
|
||||||
|
DEPLOYMENT_NAME=amar
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# NETWORK
|
||||||
|
# =============================================================================
|
||||||
|
NETWORK_NAME=amar_network
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PATHS (absolute paths to external code/dockerfiles - REQUIRED)
|
||||||
|
# =============================================================================
|
||||||
|
BACKEND_PATH=/abs/path/to/amar_django_back
|
||||||
|
FRONTEND_PATH=/abs/path/to/amar_frontend
|
||||||
|
DOCKERFILE_BACKEND=/abs/path/to/Dockerfile.backend
|
||||||
|
DOCKERFILE_FRONTEND=/abs/path/to/Dockerfile.frontend
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DATABASE
|
||||||
|
# =============================================================================
|
||||||
|
# Database dump file (relative to dumps/ directory)
|
||||||
|
DB_DUMP=test.sql
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PORTS
|
||||||
|
# =============================================================================
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
|
||||||
|
# Soleprint port
|
||||||
|
SOLEPRINT_PORT=12000
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BACKEND SERVER (Uvicorn)
|
||||||
|
# =============================================================================
|
||||||
|
# Dev: 1 worker with hot reload
|
||||||
|
# Prod: 4 workers without reload
|
||||||
|
BACKEND_WORKERS=1
|
||||||
|
BACKEND_RELOAD=--reload
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
POSTGRES_DB=amarback
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=your_secure_password_here
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DJANGO
|
||||||
|
# =============================================================================
|
||||||
|
SECRET_KEY=your_django_secret_key_here
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=amar.room.mcrn.ar,localhost
|
||||||
|
DJANGO_ENV=production
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CORS
|
||||||
|
# =============================================================================
|
||||||
|
CORS_ALLOW_ALL=false
|
||||||
|
CORS_ALLOWED_ORIGINS=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GOOGLE SERVICES
|
||||||
|
# =============================================================================
|
||||||
|
SUBJECT_CALENDAR=
|
||||||
|
SHEET_ID=
|
||||||
|
RANGE_NAME=
|
||||||
|
GOOGLE_MAPS_API_KEY=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ANALYTICS
|
||||||
|
# =============================================================================
|
||||||
|
GA4_MEASUREMENT_ID=
|
||||||
|
AMPLITUDE_API_KEY=
|
||||||
|
HOTJAR_API_KEY=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MERCADO PAGO
|
||||||
|
# =============================================================================
|
||||||
|
ACCESS_TOKEN_MERCADO_PAGO=
|
||||||
|
MP_PLATFORM_ACCESS_TOKEN=
|
||||||
|
USER_ID=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WEB PUSH
|
||||||
|
# =============================================================================
|
||||||
|
WEBPUSH_VAPID_PUBLIC_KEY=
|
||||||
|
WEBPUSH_VAPID_PRIVATE_KEY=
|
||||||
|
WEBPUSH_VAPID_ADMIN_EMAIL=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# INIT
|
||||||
|
# =============================================================================
|
||||||
|
USER_PASSWORD=initial_admin_password
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FRONTEND
|
||||||
|
# =============================================================================
|
||||||
|
NEXT_PUBLIC_APP_API_URL_BACKOFFICE=
|
||||||
|
NEXT_PUBLIC_APP_API_URL_STAGE=
|
||||||
|
NEXT_PUBLIC_IS_STAGE=false
|
||||||
|
NEXT_PUBLIC_FB_PIXEL_ID=
|
||||||
|
NEXT_PUBLIC_TAG_MANAGER=
|
||||||
|
NEXT_PUBLIC_WHATSAPP_CONTACT=
|
||||||
|
NEXT_PUBLIC_API_KEY=
|
||||||
|
NEXT_PUBLIC_AMPLITUDE_API_KEY=
|
||||||
|
NEXT_PUBLIC_GMAPS_API_KEY=
|
||||||
71
cfg/amar/Dockerfile.backend
Normal file
71
cfg/amar/Dockerfile.backend
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Dockerfile for Django Backend with Uvicorn
|
||||||
|
# =============================================================================
|
||||||
|
# Usage:
|
||||||
|
# Development: WORKERS=1 RELOAD=--reload (source mounted, hot reload)
|
||||||
|
# Production: WORKERS=4 RELOAD="" (no reload, multiple workers)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 1: Base with system dependencies
|
||||||
|
# =============================================================================
|
||||||
|
FROM python:3.11-slim AS base
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies (cached layer)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
gdal-bin \
|
||||||
|
libgdal-dev \
|
||||||
|
libgeos-dev \
|
||||||
|
libproj-dev \
|
||||||
|
postgresql-client \
|
||||||
|
# WeasyPrint dependencies (for django-afip PDF generation)
|
||||||
|
libglib2.0-0 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libgdk-pixbuf-2.0-0 \
|
||||||
|
libffi-dev \
|
||||||
|
libcairo2 \
|
||||||
|
libgirepository1.0-dev \
|
||||||
|
gir1.2-pango-1.0 \
|
||||||
|
fonts-dejavu-core \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 2: Dependencies (cached layer)
|
||||||
|
# =============================================================================
|
||||||
|
FROM base AS deps
|
||||||
|
|
||||||
|
# Copy only requirements for dependency installation
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 3: Runtime (uvicorn with configurable workers)
|
||||||
|
# =============================================================================
|
||||||
|
FROM base AS runtime
|
||||||
|
|
||||||
|
# Copy dependencies from deps stage
|
||||||
|
COPY --from=deps /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
||||||
|
COPY --from=deps /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
|
# Copy requirements (for reference)
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
RUN mkdir -p /var/etc/static /app/media
|
||||||
|
|
||||||
|
# Note: Source code mounted at runtime for dev, copied for prod
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Uvicorn with configurable workers and reload
|
||||||
|
# Dev: WORKERS=1 RELOAD=--reload
|
||||||
|
# Prod: WORKERS=4 RELOAD=""
|
||||||
|
CMD uvicorn amar_django_back.asgi:application --host 0.0.0.0 --port 8000 --workers ${WORKERS:-1} ${RELOAD}
|
||||||
80
cfg/amar/Dockerfile.frontend
Normal file
80
cfg/amar/Dockerfile.frontend
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Multi-stage Dockerfile for Next.js Frontend
|
||||||
|
# =============================================================================
|
||||||
|
# Usage:
|
||||||
|
# Development: docker compose up (mounts source, hot reload)
|
||||||
|
# Production: docker compose -f docker-compose.yml -f docker-compose.prod.yml up
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 1: Dependencies (cached layer)
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:18-alpine AS deps
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only package files for dependency installation
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install ALL dependencies (including devDependencies for dev mode)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 2: Development (hot reload, source mounted)
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:18-alpine AS development
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies from deps stage
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy package files (for npm scripts)
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Copy config files
|
||||||
|
COPY next.config.js postcss.config.js tailwind.config.js tsconfig.json ./
|
||||||
|
|
||||||
|
# Note: src/ and public/ are mounted at runtime for hot reload
|
||||||
|
# Start dev server
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 3: Builder (compile for production)
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy all source files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 4: Production (optimized, minimal)
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:18-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy only production dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
|
# Copy built application from builder
|
||||||
|
COPY --from=builder /app/.next ./.next
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/next.config.js ./
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
144
cfg/amar/config.json
Normal file
144
cfg/amar/config.json
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"framework": {
|
||||||
|
"name": "soleprint",
|
||||||
|
"slug": "soleprint",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Development workflow and documentation system",
|
||||||
|
"tagline": "Mapping development footprints",
|
||||||
|
"icon": "👣",
|
||||||
|
"hub_port": 12000
|
||||||
|
},
|
||||||
|
"managed": {
|
||||||
|
"name": "amar",
|
||||||
|
"repos": {
|
||||||
|
"backend": "/home/mariano/wdir/ama/amar_django_back",
|
||||||
|
"frontend": "/home/mariano/wdir/ama/amar_frontend"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"key": "data_flow",
|
||||||
|
"name": "artery",
|
||||||
|
"slug": "artery",
|
||||||
|
"title": "Artery",
|
||||||
|
"tagline": "Todo lo vital",
|
||||||
|
"icon": "💉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "documentation",
|
||||||
|
"name": "atlas",
|
||||||
|
"slug": "atlas",
|
||||||
|
"title": "Atlas",
|
||||||
|
"tagline": "Documentación accionable",
|
||||||
|
"icon": "🗺️"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "execution",
|
||||||
|
"name": "station",
|
||||||
|
"slug": "station",
|
||||||
|
"title": "Station",
|
||||||
|
"tagline": "Monitores, Entornos y Herramientas",
|
||||||
|
"icon": "🎛️"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"shared": {
|
||||||
|
"config": {
|
||||||
|
"name": "room",
|
||||||
|
"title": "Room",
|
||||||
|
"description": "Runtime environment configuration",
|
||||||
|
"plural": "rooms"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"name": "depot",
|
||||||
|
"title": "Depot",
|
||||||
|
"description": "Data storage / provisions",
|
||||||
|
"plural": "depots"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data_flow": {
|
||||||
|
"connector": {
|
||||||
|
"name": "vein",
|
||||||
|
"title": "Vein",
|
||||||
|
"description": "Stateless API connector",
|
||||||
|
"plural": "veins"
|
||||||
|
},
|
||||||
|
"mock": {
|
||||||
|
"name": "shunt",
|
||||||
|
"title": "Shunt",
|
||||||
|
"description": "Fake connector for testing",
|
||||||
|
"plural": "shunts"
|
||||||
|
},
|
||||||
|
"composed": {
|
||||||
|
"name": "pulse",
|
||||||
|
"title": "Pulse",
|
||||||
|
"description": "Composed data flow",
|
||||||
|
"plural": "pulses",
|
||||||
|
"formula": "Vein + Room + Depot"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"name": "plexus",
|
||||||
|
"title": "Plexus",
|
||||||
|
"description": "Full app with backend, frontend and DB",
|
||||||
|
"plural": "plexus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentation": {
|
||||||
|
"pattern": {
|
||||||
|
"name": "template",
|
||||||
|
"title": "Template",
|
||||||
|
"description": "Documentation pattern",
|
||||||
|
"plural": "templates"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"name": "book",
|
||||||
|
"title": "Book",
|
||||||
|
"description": "Documentation library"
|
||||||
|
},
|
||||||
|
"composed": {
|
||||||
|
"name": "book",
|
||||||
|
"title": "Book",
|
||||||
|
"description": "Composed documentation",
|
||||||
|
"plural": "books",
|
||||||
|
"formula": "Template + Depot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"utility": {
|
||||||
|
"name": "tool",
|
||||||
|
"title": "Tool",
|
||||||
|
"description": "Execution utility",
|
||||||
|
"plural": "tools"
|
||||||
|
},
|
||||||
|
"watcher": {
|
||||||
|
"name": "monitor",
|
||||||
|
"title": "Monitor",
|
||||||
|
"description": "Service monitor",
|
||||||
|
"plural": "monitors"
|
||||||
|
},
|
||||||
|
"container": {
|
||||||
|
"name": "cabinet",
|
||||||
|
"title": "Cabinet",
|
||||||
|
"description": "Tool container",
|
||||||
|
"plural": "cabinets"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"name": "desk",
|
||||||
|
"title": "Desk",
|
||||||
|
"description": "Execution workspace"
|
||||||
|
},
|
||||||
|
"workbench": {
|
||||||
|
"name": "desk",
|
||||||
|
"title": "Desk",
|
||||||
|
"description": "Work surface"
|
||||||
|
},
|
||||||
|
"composed": {
|
||||||
|
"name": "desk",
|
||||||
|
"title": "Desk",
|
||||||
|
"description": "Composed execution bundle",
|
||||||
|
"plural": "desks",
|
||||||
|
"formula": "Cabinet + Room + Depots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
cfg/amar/ctrl/.env.sync
Normal file
9
cfg/amar/ctrl/.env.sync
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Configuration for amar deployment
|
||||||
|
|
||||||
|
# Local source code paths
|
||||||
|
LOCAL_AMAR_BACKEND=/home/mariano/wdir/ama/amar_django_back
|
||||||
|
LOCAL_AMAR_FRONTEND=/home/mariano/wdir/ama/amar_frontend
|
||||||
|
|
||||||
|
# Test sync paths
|
||||||
|
TEST_SOURCE_PATH=/home/mariano/wdir/ama/amar_django_back/tests/contracts
|
||||||
|
WARD_TESTS_PATH=/home/mariano/wdir/ama/soleprint/ward/tools/tester/tests
|
||||||
33
cfg/amar/ctrl/logs.sh
Executable file
33
cfg/amar/ctrl/logs.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# View amar room logs
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./logs.sh # All logs (tail)
|
||||||
|
# ./logs.sh -f # Follow mode
|
||||||
|
# ./logs.sh backend # Specific container
|
||||||
|
# ./logs.sh soleprint # Soleprint logs
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
FOLLOW=""
|
||||||
|
TARGET=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
-f|--follow) FOLLOW="-f" ;;
|
||||||
|
*) TARGET="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$TARGET" ]; then
|
||||||
|
echo "=== Amar ==="
|
||||||
|
docker compose logs --tail=20 $FOLLOW
|
||||||
|
echo ""
|
||||||
|
echo "=== Soleprint ==="
|
||||||
|
(cd soleprint && docker compose logs --tail=20 $FOLLOW)
|
||||||
|
elif [ "$TARGET" = "soleprint" ]; then
|
||||||
|
(cd soleprint && docker compose logs $FOLLOW)
|
||||||
|
else
|
||||||
|
docker logs $FOLLOW "amar_$TARGET" 2>/dev/null || docker logs $FOLLOW "$TARGET"
|
||||||
|
fi
|
||||||
40
cfg/amar/ctrl/start.sh
Executable file
40
cfg/amar/ctrl/start.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Start amar room (managed app + soleprint)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./start.sh # Start all (foreground)
|
||||||
|
# ./start.sh -d # Start all (detached)
|
||||||
|
# ./start.sh amar # Start only amar
|
||||||
|
# ./start.sh soleprint # Start only soleprint
|
||||||
|
# ./start.sh --build # Rebuild images
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
BUILD=""
|
||||||
|
DETACH=""
|
||||||
|
TARGET="all"
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
-d|--detached) DETACH="-d" ;;
|
||||||
|
--build) BUILD="--build" ;;
|
||||||
|
amar) TARGET="amar" ;;
|
||||||
|
soleprint) TARGET="soleprint" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$TARGET" = "all" ] || [ "$TARGET" = "amar" ]; then
|
||||||
|
echo "Starting amar..."
|
||||||
|
docker compose up $DETACH $BUILD
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TARGET" = "all" ] || [ "$TARGET" = "soleprint" ]; then
|
||||||
|
echo "Starting soleprint..."
|
||||||
|
(cd soleprint && docker compose up $DETACH $BUILD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$DETACH" ]; then
|
||||||
|
echo ""
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(amar|soleprint|NAMES)"
|
||||||
|
fi
|
||||||
7
cfg/amar/ctrl/status.sh
Executable file
7
cfg/amar/ctrl/status.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Show amar room status
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "=== Containers ==="
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(amar|soleprint|NAMES)" || echo "No containers running"
|
||||||
25
cfg/amar/ctrl/stop.sh
Executable file
25
cfg/amar/ctrl/stop.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Stop amar room (managed app + soleprint)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./stop.sh # Stop all
|
||||||
|
# ./stop.sh amar # Stop only amar
|
||||||
|
# ./stop.sh soleprint # Stop only soleprint
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
TARGET="all"
|
||||||
|
[ -n "$1" ] && TARGET="$1"
|
||||||
|
|
||||||
|
if [ "$TARGET" = "all" ] || [ "$TARGET" = "soleprint" ]; then
|
||||||
|
echo "Stopping soleprint..."
|
||||||
|
(cd soleprint && docker compose down)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TARGET" = "all" ] || [ "$TARGET" = "amar" ]; then
|
||||||
|
echo "Stopping amar..."
|
||||||
|
docker compose down
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
117
cfg/amar/ctrl/xtras/reload-db.sh
Executable file
117
cfg/amar/ctrl/xtras/reload-db.sh
Executable file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Smart database reload - only swaps if DB_DUMP changed
|
||||||
|
#
|
||||||
|
# Tracks which dump is currently loaded and only reloads if different.
|
||||||
|
# Edit DB_DUMP in .env and run this script - it handles the rest.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./reload-db.sh # Reload if DB_DUMP changed
|
||||||
|
# ./reload-db.sh --force # Force reload even if same
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
FORCE=false
|
||||||
|
if [ "$1" = "--force" ]; then
|
||||||
|
FORCE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get config from .env
|
||||||
|
DEPLOYMENT_NAME=$(grep "^DEPLOYMENT_NAME=" .env 2>/dev/null | cut -d'=' -f2 || echo "amar")
|
||||||
|
POSTGRES_DB=$(grep "^POSTGRES_DB=" .env 2>/dev/null | cut -d'=' -f2 || echo "amarback")
|
||||||
|
POSTGRES_USER=$(grep "^POSTGRES_USER=" .env 2>/dev/null | cut -d'=' -f2 || echo "postgres")
|
||||||
|
DB_DUMP=$(grep "^DB_DUMP=" .env 2>/dev/null | cut -d'=' -f2)
|
||||||
|
|
||||||
|
if [ -z "$DB_DUMP" ]; then
|
||||||
|
echo "Error: DB_DUMP not set in .env"
|
||||||
|
echo ""
|
||||||
|
echo "Add to .env:"
|
||||||
|
echo " DB_DUMP=dev.sql"
|
||||||
|
echo ""
|
||||||
|
echo "Available dumps:"
|
||||||
|
ls -1 dumps/*.sql 2>/dev/null | sed 's/dumps\// /' || echo " No dumps found in dumps/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DUMP_FILE="dumps/${DB_DUMP}"
|
||||||
|
if [ ! -f "$DUMP_FILE" ]; then
|
||||||
|
echo "Error: Dump file not found: $DUMP_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "Available dumps:"
|
||||||
|
ls -1 dumps/*.sql 2>/dev/null | sed 's/dumps\// /' || echo " No dumps found in dumps/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_CONTAINER="${DEPLOYMENT_NAME}_db"
|
||||||
|
BACKEND_CONTAINER="${DEPLOYMENT_NAME}_backend"
|
||||||
|
STATE_FILE=".db_state"
|
||||||
|
|
||||||
|
# Check if db container is running
|
||||||
|
if ! docker ps --format "{{.Names}}" | grep -q "^${DB_CONTAINER}$"; then
|
||||||
|
echo "Error: Database container not running: $DB_CONTAINER"
|
||||||
|
echo "Start services first with: ./ctrl/start.sh -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check current state
|
||||||
|
if [ -f "$STATE_FILE" ] && [ "$FORCE" = false ]; then
|
||||||
|
CURRENT_DUMP=$(cat "$STATE_FILE")
|
||||||
|
if [ "$CURRENT_DUMP" = "$DB_DUMP" ]; then
|
||||||
|
echo "Database already loaded with: $DB_DUMP"
|
||||||
|
echo "Use --force to reload anyway"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Database dump changed: $CURRENT_DUMP → $DB_DUMP"
|
||||||
|
else
|
||||||
|
if [ "$FORCE" = true ]; then
|
||||||
|
echo "Force reloading database with: $DB_DUMP"
|
||||||
|
else
|
||||||
|
echo "Loading database with: $DB_DUMP"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
read -p "Continue with database reload? (y/N) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[1/5] Stopping backend and celery services..."
|
||||||
|
docker compose stop backend celery celery-beat 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[2/5] Dropping and recreating database..."
|
||||||
|
docker exec "$DB_CONTAINER" psql -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE IF EXISTS $POSTGRES_DB WITH (FORCE);"
|
||||||
|
docker exec "$DB_CONTAINER" psql -U "$POSTGRES_USER" -d postgres -c "CREATE DATABASE $POSTGRES_DB;"
|
||||||
|
docker exec "$DB_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS postgis_topology;"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[3/5] Loading dump: $DB_DUMP..."
|
||||||
|
docker exec -i "$DB_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$DUMP_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[4/5] Restarting services and running migrations..."
|
||||||
|
docker compose start backend celery celery-beat
|
||||||
|
|
||||||
|
echo "Waiting for backend to start..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "Running migrations..."
|
||||||
|
docker exec "$BACKEND_CONTAINER" python manage.py migrate --noinput
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[5/5] Updating state..."
|
||||||
|
echo "$DB_DUMP" > "$STATE_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Database Reloaded Successfully"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Current dump: $DB_DUMP"
|
||||||
|
echo "Database: $POSTGRES_DB"
|
||||||
|
echo ""
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Soleprint Data Layer
|
Pawprint Data Layer
|
||||||
|
|
||||||
JSON file storage (future: MongoDB)
|
JSON file storage (future: MongoDB)
|
||||||
"""
|
"""
|
||||||
79
cfg/amar/data/books.json
Normal file
79
cfg/amar/data/books.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "arch-model",
|
||||||
|
"slug": "arch-model",
|
||||||
|
"title": "Architecture Model",
|
||||||
|
"status": "ready",
|
||||||
|
"template": null,
|
||||||
|
"larder": {
|
||||||
|
"name": "arch-model",
|
||||||
|
"slug": "arch-model",
|
||||||
|
"title": "Architecture Model",
|
||||||
|
"status": "ready",
|
||||||
|
"source_template": null,
|
||||||
|
"data_path": "album/book/arch-model"
|
||||||
|
},
|
||||||
|
"output_larder": null,
|
||||||
|
"system": "album"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feature-flow",
|
||||||
|
"slug": "feature-flow",
|
||||||
|
"title": "Feature Flow Pipeline",
|
||||||
|
"status": "ready",
|
||||||
|
"template": null,
|
||||||
|
"larder": {
|
||||||
|
"name": "feature-flow",
|
||||||
|
"slug": "feature-flow",
|
||||||
|
"title": "Feature Flow Pipeline",
|
||||||
|
"status": "ready",
|
||||||
|
"source_template": null,
|
||||||
|
"data_path": "album/book/feature-flow"
|
||||||
|
},
|
||||||
|
"output_larder": null,
|
||||||
|
"system": "album"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gherkin-samples",
|
||||||
|
"slug": "gherkin-samples",
|
||||||
|
"title": "Gherkin Samples",
|
||||||
|
"status": "ready",
|
||||||
|
"template": null,
|
||||||
|
"larder": {
|
||||||
|
"name": "gherkin-samples",
|
||||||
|
"slug": "gherkin-samples",
|
||||||
|
"title": "Gherkin Samples",
|
||||||
|
"status": "ready",
|
||||||
|
"source_template": null,
|
||||||
|
"data_path": "album/book/gherkin-samples"
|
||||||
|
},
|
||||||
|
"output_larder": null,
|
||||||
|
"system": "album"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feature-form-samples",
|
||||||
|
"slug": "feature-form-samples",
|
||||||
|
"title": "Feature Form Samples",
|
||||||
|
"status": "ready",
|
||||||
|
"template": {
|
||||||
|
"name": "feature-form",
|
||||||
|
"slug": "feature-form",
|
||||||
|
"title": "Feature Form Template",
|
||||||
|
"status": "ready",
|
||||||
|
"template_path": "album/template/feature-form",
|
||||||
|
"system": "album"
|
||||||
|
},
|
||||||
|
"larder": {
|
||||||
|
"name": "feature-form",
|
||||||
|
"slug": "feature-form",
|
||||||
|
"title": "Feature Forms",
|
||||||
|
"status": "ready",
|
||||||
|
"source_template": "feature-form",
|
||||||
|
"data_path": "album/book/feature-form-samples/feature-form"
|
||||||
|
},
|
||||||
|
"output_larder": null,
|
||||||
|
"system": "album"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
cfg/amar/data/depots.json
Normal file
12
cfg/amar/data/depots.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "feature-form",
|
||||||
|
"slug": "feature-form",
|
||||||
|
"title": "Feature Forms",
|
||||||
|
"status": "ready",
|
||||||
|
"source_template": "feature-form",
|
||||||
|
"data_path": "album/book/feature-form-samples/feature-form"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
cfg/amar/data/monitors.json
Normal file
22
cfg/amar/data/monitors.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "turnos",
|
||||||
|
"slug": "turnos",
|
||||||
|
"title": "Turnos Monitor",
|
||||||
|
"status": "dev",
|
||||||
|
"system": "ward",
|
||||||
|
"description": "Pipeline view of requests → turnos. Shows vet-petowner at a glance.",
|
||||||
|
"path": "ward/monitor/turnos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data_browse",
|
||||||
|
"slug": "data-browse",
|
||||||
|
"title": "Data Browse",
|
||||||
|
"status": "ready",
|
||||||
|
"system": "ward",
|
||||||
|
"description": "Quick navigation to test users and data states. Book/larder pattern with SQL mode for manual testing workflows.",
|
||||||
|
"path": "ward/monitor/data_browse"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
cfg/amar/data/rooms.json
Normal file
5
cfg/amar/data/rooms.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{"name": "pawprint-local", "slug": "pawprint-local", "title": "Pawprint Local", "status": "dev", "config_path": "deploy/pawprint-local"}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
cfg/amar/data/template/feature-form/template.md
Normal file
38
cfg/amar/data/template/feature-form/template.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# {{nombre_flujo}}
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
{{tipo_usuario}}
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
{{punto_entrada}}
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
{{objetivo}}
|
||||||
|
|
||||||
|
## Pasos
|
||||||
|
|
||||||
|
1. {{paso_1}}
|
||||||
|
2. {{paso_2}}
|
||||||
|
3. {{paso_3}}
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- {{resultado_1}}
|
||||||
|
- {{resultado_2}}
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- {{problema_1}}
|
||||||
|
- {{problema_2}}
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- {{caso_especial_1}}
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- {{flujo_relacionado_1}}
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- {{nota_tecnica_1}}
|
||||||
12
cfg/amar/data/templates.json
Normal file
12
cfg/amar/data/templates.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "feature-form",
|
||||||
|
"slug": "feature-form",
|
||||||
|
"title": "Feature Form Template",
|
||||||
|
"status": "ready",
|
||||||
|
"template_path": "data/template/feature-form",
|
||||||
|
"system": "album"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
cfg/amar/data/tools.json
Normal file
48
cfg/amar/data/tools.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "tester",
|
||||||
|
"slug": "tester",
|
||||||
|
"title": "Contract Tests",
|
||||||
|
"status": "live",
|
||||||
|
"system": "ward",
|
||||||
|
"type": "app",
|
||||||
|
"description": "HTTP contract test runner with multi-environment support. Filter, run, and track tests against dev/stage/prod.",
|
||||||
|
"path": "ward/tools/tester",
|
||||||
|
"url": "/tools/tester/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "datagen",
|
||||||
|
"slug": "datagen",
|
||||||
|
"title": "Test Data Generator",
|
||||||
|
"status": "live",
|
||||||
|
"system": "ward",
|
||||||
|
"type": "cli",
|
||||||
|
"description": "Generate realistic test data for Amar domain (users, pets, services) and MercadoPago API responses. Used by mock veins and test seeders.",
|
||||||
|
"path": "ward/tools/datagen",
|
||||||
|
"cli": "python -m datagen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "generate_test_data",
|
||||||
|
"slug": "generate-test-data",
|
||||||
|
"title": "DB Test Data Extractor",
|
||||||
|
"status": "dev",
|
||||||
|
"system": "ward",
|
||||||
|
"type": "cli",
|
||||||
|
"description": "Extract representative subsets from PostgreSQL dumps for testing/development.",
|
||||||
|
"path": "ward/tools/generate_test_data",
|
||||||
|
"cli": "python -m generate_test_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "modelgen",
|
||||||
|
"slug": "modelgen",
|
||||||
|
"title": "Model Generator",
|
||||||
|
"status": "dev",
|
||||||
|
"system": "ward",
|
||||||
|
"type": "cli",
|
||||||
|
"description": "Generate platform-specific models (Pydantic, Django, Prisma) from JSON Schema.",
|
||||||
|
"path": "ward/tools/modelgen",
|
||||||
|
"cli": "python -m modelgen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
60
cfg/amar/data/veins.json
Normal file
60
cfg/amar/data/veins.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "jira",
|
||||||
|
"slug": "jira",
|
||||||
|
"title": "Jira",
|
||||||
|
"status": "live",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slack",
|
||||||
|
"slug": "slack",
|
||||||
|
"title": "Slack",
|
||||||
|
"status": "building",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "google",
|
||||||
|
"slug": "google",
|
||||||
|
"title": "Google",
|
||||||
|
"status": "planned",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maps",
|
||||||
|
"slug": "maps",
|
||||||
|
"title": "Maps",
|
||||||
|
"status": "planned",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whatsapp",
|
||||||
|
"slug": "whatsapp",
|
||||||
|
"title": "WhatsApp",
|
||||||
|
"status": "planned",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gnucash",
|
||||||
|
"slug": "gnucash",
|
||||||
|
"title": "GNUCash",
|
||||||
|
"status": "planned",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vnc",
|
||||||
|
"slug": "vnc",
|
||||||
|
"title": "VNC",
|
||||||
|
"status": "planned",
|
||||||
|
"system": "artery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ia",
|
||||||
|
"slug": "ia",
|
||||||
|
"title": "IA",
|
||||||
|
"status": "planned",
|
||||||
|
"system": "artery"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
202
cfg/amar/docker-compose.yml
Normal file
202
cfg/amar/docker-compose.yml
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Amar Room - Docker Compose
|
||||||
|
#
|
||||||
|
# Creates: db, redis, backend, celery, celery-beat, frontend
|
||||||
|
# Network: Joins external network defined by NETWORK_NAME
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd cfg/amar && docker compose up -d
|
||||||
|
#
|
||||||
|
# Required Environment Variables (from .env):
|
||||||
|
# - DEPLOYMENT_NAME: Prefix for container names
|
||||||
|
# - NETWORK_NAME: Network to join
|
||||||
|
# - BACKEND_PATH, FRONTEND_PATH: Source code paths (absolute)
|
||||||
|
# - DOCKERFILE_BACKEND, DOCKERFILE_FRONTEND: Dockerfile paths (absolute)
|
||||||
|
# - DB_DUMP: Database dump file (relative to dumps/)
|
||||||
|
# - Plus all app-specific vars (POSTGRES_*, etc.)
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:15-3.4
|
||||||
|
container_name: ${DEPLOYMENT_NAME}_db
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
# Database dump loaded on init (if volume is empty)
|
||||||
|
- ./dumps/${DB_DUMP}:/docker-entrypoint-initdb.d/dump.sql:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ${DEPLOYMENT_NAME}_redis
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ${BACKEND_PATH}
|
||||||
|
dockerfile: ${DOCKERFILE_BACKEND}
|
||||||
|
target: runtime
|
||||||
|
container_name: ${DEPLOYMENT_NAME}_backend
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY
|
||||||
|
- DEBUG
|
||||||
|
- ALLOWED_HOSTS
|
||||||
|
- DJANGO_ENV
|
||||||
|
- DB_NAME=${POSTGRES_DB}
|
||||||
|
- DB_USER=${POSTGRES_USER}
|
||||||
|
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
- CORS_ALLOW_ALL
|
||||||
|
- CORS_ALLOWED_ORIGINS
|
||||||
|
# Uvicorn config (dev: 1 worker + reload, prod: 4 workers)
|
||||||
|
- WORKERS=${BACKEND_WORKERS:-1}
|
||||||
|
- RELOAD=${BACKEND_RELOAD:---reload}
|
||||||
|
# Google
|
||||||
|
- SUBJECT_CALENDAR
|
||||||
|
- SHEET_ID
|
||||||
|
- RANGE_NAME
|
||||||
|
- GOOGLE_MAPS_API_KEY
|
||||||
|
# Analytics
|
||||||
|
- GA4_MEASUREMENT_ID
|
||||||
|
- AMPLITUDE_API_KEY
|
||||||
|
- HOTJAR_API_KEY
|
||||||
|
# Payments
|
||||||
|
- ACCESS_TOKEN_MERCADO_PAGO
|
||||||
|
- MP_PLATFORM_ACCESS_TOKEN
|
||||||
|
- USER_ID
|
||||||
|
# Push
|
||||||
|
- WEBPUSH_VAPID_PUBLIC_KEY
|
||||||
|
- WEBPUSH_VAPID_PRIVATE_KEY
|
||||||
|
- WEBPUSH_VAPID_ADMIN_EMAIL
|
||||||
|
# Init
|
||||||
|
- USER_PASSWORD
|
||||||
|
volumes:
|
||||||
|
- ${BACKEND_PATH}:/app
|
||||||
|
- backend_static:/var/etc/static
|
||||||
|
- backend_media:/app/media
|
||||||
|
ports:
|
||||||
|
- "${BACKEND_PORT}:8000"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate --noinput &&
|
||||||
|
uvicorn amar_django_back.asgi:application --host 0.0.0.0 --port 8000 --workers $${WORKERS:-1} $${RELOAD}"
|
||||||
|
|
||||||
|
celery:
|
||||||
|
build:
|
||||||
|
context: ${BACKEND_PATH}
|
||||||
|
dockerfile: ${DOCKERFILE_BACKEND}
|
||||||
|
target: runtime
|
||||||
|
container_name: ${DEPLOYMENT_NAME}_celery
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY
|
||||||
|
- DEBUG
|
||||||
|
- DJANGO_ENV
|
||||||
|
- DB_NAME=${POSTGRES_DB}
|
||||||
|
- DB_USER=${POSTGRES_USER}
|
||||||
|
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
volumes:
|
||||||
|
- ${BACKEND_PATH}:/app
|
||||||
|
- backend_media:/app/media
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
command: celery -A amar_django_back worker -l INFO
|
||||||
|
|
||||||
|
celery-beat:
|
||||||
|
build:
|
||||||
|
context: ${BACKEND_PATH}
|
||||||
|
dockerfile: ${DOCKERFILE_BACKEND}
|
||||||
|
target: runtime
|
||||||
|
container_name: ${DEPLOYMENT_NAME}_celery_beat
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY
|
||||||
|
- DEBUG
|
||||||
|
- DJANGO_ENV
|
||||||
|
- DB_NAME=${POSTGRES_DB}
|
||||||
|
- DB_USER=${POSTGRES_USER}
|
||||||
|
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
volumes:
|
||||||
|
- ${BACKEND_PATH}:/app
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
command: celery -A amar_django_back beat -l INFO
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ${FRONTEND_PATH}
|
||||||
|
dockerfile: ${DOCKERFILE_FRONTEND}
|
||||||
|
target: development
|
||||||
|
container_name: ${DEPLOYMENT_NAME}_frontend
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_APP_API_URL_BACKOFFICE
|
||||||
|
- NEXT_PUBLIC_APP_API_URL_STAGE
|
||||||
|
- NEXT_PUBLIC_IS_STAGE
|
||||||
|
- NEXT_PUBLIC_FB_PIXEL_ID
|
||||||
|
- NEXT_PUBLIC_TAG_MANAGER
|
||||||
|
- NEXT_PUBLIC_WHATSAPP_CONTACT
|
||||||
|
- NEXT_PUBLIC_API_KEY
|
||||||
|
- NEXT_PUBLIC_AMPLITUDE_API_KEY
|
||||||
|
- NEXT_PUBLIC_GMAPS_API_KEY
|
||||||
|
volumes:
|
||||||
|
- ${FRONTEND_PATH}/src:/app/src
|
||||||
|
- ${FRONTEND_PATH}/public:/app/public
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT}:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
# CMD from Dockerfile.frontend development stage: npm run dev
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
redisdata:
|
||||||
|
backend_static:
|
||||||
|
backend_media:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external: true
|
||||||
|
name: ${NETWORK_NAME}
|
||||||
13
cfg/amar/link/Dockerfile
Normal file
13
cfg/amar/link/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run
|
||||||
|
CMD ["python", "main.py"]
|
||||||
120
cfg/amar/link/README.md
Normal file
120
cfg/amar/link/README.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Link Room - Adapter Layer
|
||||||
|
|
||||||
|
Provides framework-agnostic data navigation between managed apps (AMAR) and soleprint.
|
||||||
|
|
||||||
|
## Status: Initial Implementation ✅
|
||||||
|
|
||||||
|
**Working:**
|
||||||
|
- ✅ FastAPI service with adapter pattern
|
||||||
|
- ✅ BaseAdapter interface for pluggable frameworks
|
||||||
|
- ✅ DjangoAdapter with AMAR database queries
|
||||||
|
- ✅ Docker build and container starts
|
||||||
|
- ✅ `/health` endpoint (adapter loads successfully)
|
||||||
|
- ✅ `/api/queries` endpoint (lists available queries)
|
||||||
|
|
||||||
|
**Pending:**
|
||||||
|
- ⏳ Database connection (needs DB_HOST env var fix)
|
||||||
|
- ⏳ Complete all entity queries (Pet, Vet, ServiceRequest, etc.)
|
||||||
|
- ⏳ Ward integration (consume JSON and render graph)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Managed App (AMAR) ←─── link_room ───→ Soleprint (Ward)
|
||||||
|
↓ ↓ ↓
|
||||||
|
Database Adapter Layer Graph Renderer
|
||||||
|
(SQL → JSON) (JSON → SVG)
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON Contract:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{"id": "User_123", "type": "User", "label": "john", "data": {...}}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"from": "User_123", "to": "PetOwner_456", "label": "has profile"}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"title": "User #123",
|
||||||
|
"credentials": "john | Password: Amar2025!",
|
||||||
|
"fields": {"Email": "john@example.com"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- `GET /health` - Health check with adapter status
|
||||||
|
- `GET /api/queries` - List available predefined queries
|
||||||
|
- `GET /api/navigate?query=user_with_pets` - Query mode
|
||||||
|
- `GET /api/navigate?entity=User&id=123` - Entity navigation mode
|
||||||
|
|
||||||
|
## Available Queries
|
||||||
|
|
||||||
|
1. `user_with_pets` - User with Pet ownership
|
||||||
|
2. `user_with_requests` - User with ServiceRequests
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start (from core_room/ctrl)
|
||||||
|
./start.sh link_room -d --build
|
||||||
|
|
||||||
|
# Test
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
curl http://localhost:8100/api/queries
|
||||||
|
curl "http://localhost:8100/api/navigate?query=user_with_pets"
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
docker logs core_room_link_room
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
From `core_room/.env`:
|
||||||
|
- `NEST_NAME` - Container naming
|
||||||
|
- `NETWORK_NAME` - Docker network
|
||||||
|
- `DB_HOST` - Database host (needs fix: should point to db container)
|
||||||
|
- `DB_PORT` - Database port
|
||||||
|
- `DB_NAME` - Database name
|
||||||
|
- `DB_USER` - Database user
|
||||||
|
- `DB_PASSWORD` - Database password
|
||||||
|
- `ADAPTER_TYPE` - Adapter to use (default: django)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Fix DB connection** - Set correct DB_HOST in core_room/.env
|
||||||
|
2. **Complete queries** - Add remaining entity types
|
||||||
|
3. **Ward integration** - Create ward consumer for JSON
|
||||||
|
4. **Add graphviz rendering** - Move from data_browse reference
|
||||||
|
5. **Test end-to-end** - Query → JSON → SVG → Display
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
link_room/
|
||||||
|
├── README.md # This file
|
||||||
|
├── main.py # FastAPI app with endpoints
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── Dockerfile # Container build
|
||||||
|
├── docker-compose.yml # Service definition
|
||||||
|
└── adapters/
|
||||||
|
├── __init__.py # BaseAdapter interface
|
||||||
|
└── django.py # DjangoAdapter implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
|
||||||
|
✅ **Framework-agnostic** - Works with Django, Rails, Express, etc.
|
||||||
|
✅ **Decoupled** - Managed app owns data, link_room translates
|
||||||
|
✅ **Pluggable** - Adapters for different frameworks
|
||||||
|
✅ **Authenticated** - Ready for remote deployment
|
||||||
|
✅ **Incremental** - Build and test each piece
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
Previous approach (databrowse direct DB) saved in:
|
||||||
|
- Branch: `ref/databrowse-direct-db` (ward repo)
|
||||||
|
- Problem: Tight coupling, won't work remote
|
||||||
|
- Solution: This adapter pattern
|
||||||
43
cfg/amar/link/adapters/__init__.py
Normal file
43
cfg/amar/link/adapters/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Adapters for different managed app frameworks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAdapter(ABC):
|
||||||
|
"""Base adapter interface."""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Initialize adapter with configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Database connection or API endpoint configuration
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def navigate(
|
||||||
|
self,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
entity: Optional[str] = None,
|
||||||
|
id: Optional[int] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Navigate data graph.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"nodes": [{"id": str, "type": str, "label": str, "data": dict}],
|
||||||
|
"edges": [{"from": str, "to": str, "label": str}],
|
||||||
|
"summary": {"title": str, "credentials": str|None, "fields": dict}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_queries(self) -> List[str]:
|
||||||
|
"""Return list of available query names."""
|
||||||
|
pass
|
||||||
235
cfg/amar/link/adapters/django.py
Normal file
235
cfg/amar/link/adapters/django.py
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
"""
|
||||||
|
Django adapter for AMAR.
|
||||||
|
|
||||||
|
Queries AMAR's PostgreSQL database directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
from . import BaseAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoAdapter(BaseAdapter):
|
||||||
|
"""Adapter for Django/AMAR."""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]):
|
||||||
|
super().__init__(config)
|
||||||
|
self.engine = self._create_engine()
|
||||||
|
|
||||||
|
def _create_engine(self):
|
||||||
|
"""Create SQLAlchemy engine from config."""
|
||||||
|
db_url = (
|
||||||
|
f"postgresql://{self.config['user']}:{self.config['password']}"
|
||||||
|
f"@{self.config['host']}:{self.config['port']}/{self.config['name']}"
|
||||||
|
)
|
||||||
|
return create_engine(db_url, pool_pre_ping=True)
|
||||||
|
|
||||||
|
def _execute(self, sql: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Execute SQL and return results as list of dicts."""
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
result = conn.execute(text(sql))
|
||||||
|
rows = result.fetchall()
|
||||||
|
columns = result.keys()
|
||||||
|
return [dict(zip(columns, row)) for row in rows]
|
||||||
|
|
||||||
|
def get_queries(self) -> List[str]:
|
||||||
|
"""Available predefined queries."""
|
||||||
|
return [
|
||||||
|
"user_with_pets",
|
||||||
|
"user_with_requests",
|
||||||
|
]
|
||||||
|
|
||||||
|
def navigate(
|
||||||
|
self,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
entity: Optional[str] = None,
|
||||||
|
id: Optional[int] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Navigate data graph."""
|
||||||
|
|
||||||
|
if query:
|
||||||
|
return self._query_mode(query)
|
||||||
|
elif entity and id:
|
||||||
|
return self._entity_mode(entity, id)
|
||||||
|
else:
|
||||||
|
raise ValueError("Must provide either query or entity+id")
|
||||||
|
|
||||||
|
def _query_mode(self, query_name: str) -> Dict[str, Any]:
|
||||||
|
"""Execute predefined query."""
|
||||||
|
|
||||||
|
if query_name == "user_with_pets":
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
u.id as user_id, u.username, u.email,
|
||||||
|
po.id as petowner_id, po.first_name, po.last_name, po.phone,
|
||||||
|
p.id as pet_id, p.name as pet_name, p.pet_type, p.age
|
||||||
|
FROM auth_user u
|
||||||
|
JOIN mascotas_petowner po ON po.user_id = u.id
|
||||||
|
JOIN mascotas_pet p ON p.owner_id = po.id
|
||||||
|
WHERE p.deleted = false
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
elif query_name == "user_with_requests":
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
u.id as user_id, u.username, u.email,
|
||||||
|
po.id as petowner_id, po.first_name, po.last_name,
|
||||||
|
sr.id as request_id, sr.state, sr.created_at
|
||||||
|
FROM auth_user u
|
||||||
|
JOIN mascotas_petowner po ON po.user_id = u.id
|
||||||
|
JOIN solicitudes_servicerequest sr ON sr.petowner_id = po.id
|
||||||
|
WHERE sr.deleted = false
|
||||||
|
ORDER BY sr.created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown query: {query_name}")
|
||||||
|
|
||||||
|
rows = self._execute(sql)
|
||||||
|
if not rows:
|
||||||
|
return self._empty_response()
|
||||||
|
|
||||||
|
return self._rows_to_graph(rows[0])
|
||||||
|
|
||||||
|
def _entity_mode(self, entity: str, id: int) -> Dict[str, Any]:
|
||||||
|
"""Navigate to specific entity."""
|
||||||
|
|
||||||
|
if entity == "User":
|
||||||
|
sql = f"""
|
||||||
|
SELECT
|
||||||
|
u.id as user_id, u.username, u.email,
|
||||||
|
po.id as petowner_id, po.first_name, po.last_name, po.phone
|
||||||
|
FROM auth_user u
|
||||||
|
LEFT JOIN mascotas_petowner po ON po.user_id = u.id
|
||||||
|
WHERE u.id = {id}
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown entity: {entity}")
|
||||||
|
|
||||||
|
rows = self._execute(sql)
|
||||||
|
if not rows:
|
||||||
|
return self._empty_response()
|
||||||
|
|
||||||
|
return self._rows_to_graph(rows[0])
|
||||||
|
|
||||||
|
def _rows_to_graph(self, row: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Convert SQL row to graph structure."""
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
# User node
|
||||||
|
if "user_id" in row and row["user_id"]:
|
||||||
|
nodes.append({
|
||||||
|
"id": f"User_{row['user_id']}",
|
||||||
|
"type": "User",
|
||||||
|
"label": row.get("username") or row.get("email", ""),
|
||||||
|
"data": {
|
||||||
|
"id": row["user_id"],
|
||||||
|
"username": row.get("username"),
|
||||||
|
"email": row.get("email"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# PetOwner node
|
||||||
|
if "petowner_id" in row and row["petowner_id"]:
|
||||||
|
name = f"{row.get('first_name', '')} {row.get('last_name', '')}".strip()
|
||||||
|
nodes.append({
|
||||||
|
"id": f"PetOwner_{row['petowner_id']}",
|
||||||
|
"type": "PetOwner",
|
||||||
|
"label": name or "PetOwner",
|
||||||
|
"data": {
|
||||||
|
"id": row["petowner_id"],
|
||||||
|
"first_name": row.get("first_name"),
|
||||||
|
"last_name": row.get("last_name"),
|
||||||
|
"phone": row.get("phone"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if "user_id" in row and row["user_id"]:
|
||||||
|
edges.append({
|
||||||
|
"from": f"User_{row['user_id']}",
|
||||||
|
"to": f"PetOwner_{row['petowner_id']}",
|
||||||
|
"label": "has profile"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Pet node
|
||||||
|
if "pet_id" in row and row["pet_id"]:
|
||||||
|
nodes.append({
|
||||||
|
"id": f"Pet_{row['pet_id']}",
|
||||||
|
"type": "Pet",
|
||||||
|
"label": row.get("pet_name", "Pet"),
|
||||||
|
"data": {
|
||||||
|
"id": row["pet_id"],
|
||||||
|
"name": row.get("pet_name"),
|
||||||
|
"pet_type": row.get("pet_type"),
|
||||||
|
"age": row.get("age"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if "petowner_id" in row and row["petowner_id"]:
|
||||||
|
edges.append({
|
||||||
|
"from": f"PetOwner_{row['petowner_id']}",
|
||||||
|
"to": f"Pet_{row['pet_id']}",
|
||||||
|
"label": "owns"
|
||||||
|
})
|
||||||
|
|
||||||
|
# ServiceRequest node
|
||||||
|
if "request_id" in row and row["request_id"]:
|
||||||
|
nodes.append({
|
||||||
|
"id": f"ServiceRequest_{row['request_id']}",
|
||||||
|
"type": "ServiceRequest",
|
||||||
|
"label": f"Request #{row['request_id']}",
|
||||||
|
"data": {
|
||||||
|
"id": row["request_id"],
|
||||||
|
"state": row.get("state"),
|
||||||
|
"created_at": str(row.get("created_at", "")),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if "petowner_id" in row and row["petowner_id"]:
|
||||||
|
edges.append({
|
||||||
|
"from": f"PetOwner_{row['petowner_id']}",
|
||||||
|
"to": f"ServiceRequest_{row['request_id']}",
|
||||||
|
"label": "requested"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Build summary from first User node
|
||||||
|
summary = self._build_summary(nodes)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"nodes": nodes,
|
||||||
|
"edges": edges,
|
||||||
|
"summary": summary
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_summary(self, nodes: List[Dict]) -> Dict[str, Any]:
|
||||||
|
"""Build summary from nodes."""
|
||||||
|
|
||||||
|
# Find User node
|
||||||
|
user_node = next((n for n in nodes if n["type"] == "User"), None)
|
||||||
|
if user_node:
|
||||||
|
data = user_node["data"]
|
||||||
|
return {
|
||||||
|
"title": f"User #{data['id']}",
|
||||||
|
"credentials": f"{data.get('username', 'N/A')} | Password: Amar2025!",
|
||||||
|
"fields": {
|
||||||
|
"Email": data.get("email", "N/A"),
|
||||||
|
"Username": data.get("username", "N/A"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
return {
|
||||||
|
"title": "No data",
|
||||||
|
"credentials": None,
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _empty_response(self) -> Dict[str, Any]:
|
||||||
|
"""Return empty response structure."""
|
||||||
|
return {
|
||||||
|
"nodes": [],
|
||||||
|
"edges": [],
|
||||||
|
"summary": {
|
||||||
|
"title": "No data found",
|
||||||
|
"credentials": None,
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
cfg/amar/link/docker-compose.yml
Normal file
25
cfg/amar/link/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
link_nest:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ${NEST_NAME}_link_nest
|
||||||
|
ports:
|
||||||
|
- "8100:8000"
|
||||||
|
environment:
|
||||||
|
- PORT=8000
|
||||||
|
- ADAPTER_TYPE=${ADAPTER_TYPE:-django}
|
||||||
|
- DB_HOST=${DB_HOST}
|
||||||
|
- DB_PORT=${DB_PORT}
|
||||||
|
- DB_NAME=${DB_NAME}
|
||||||
|
- DB_USER=${DB_USER}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external: true
|
||||||
|
name: ${NETWORK_NAME}
|
||||||
105
cfg/amar/link/main.py
Normal file
105
cfg/amar/link/main.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
"""
|
||||||
|
Link Room - Adapter layer between managed apps and soleprint.
|
||||||
|
|
||||||
|
Exposes standardized JSON endpoints for data navigation.
|
||||||
|
Framework-agnostic via pluggable adapters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
|
||||||
|
app = FastAPI(title="Link Room", version="0.1.0")
|
||||||
|
|
||||||
|
# Lazy-loaded adapter instance
|
||||||
|
_adapter = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_adapter():
|
||||||
|
"""Get or create adapter instance."""
|
||||||
|
global _adapter
|
||||||
|
if _adapter is None:
|
||||||
|
adapter_type = os.getenv("ADAPTER_TYPE", "django")
|
||||||
|
|
||||||
|
# Database config from environment
|
||||||
|
db_config = {
|
||||||
|
"host": os.getenv("DB_HOST", "localhost"),
|
||||||
|
"port": int(os.getenv("DB_PORT", "5432")),
|
||||||
|
"name": os.getenv("DB_NAME", "amarback"),
|
||||||
|
"user": os.getenv("DB_USER", "postgres"),
|
||||||
|
"password": os.getenv("DB_PASSWORD", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if adapter_type == "django":
|
||||||
|
from adapters.django import DjangoAdapter
|
||||||
|
_adapter = DjangoAdapter(db_config)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown adapter type: {adapter_type}")
|
||||||
|
|
||||||
|
return _adapter
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
"""Health check."""
|
||||||
|
adapter_type = os.getenv("ADAPTER_TYPE", "django")
|
||||||
|
|
||||||
|
# Test adapter connection
|
||||||
|
adapter_ok = False
|
||||||
|
try:
|
||||||
|
adapter = get_adapter()
|
||||||
|
adapter_ok = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Adapter error: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok" if adapter_ok else "degraded",
|
||||||
|
"service": "link-room",
|
||||||
|
"adapter": adapter_type,
|
||||||
|
"adapter_loaded": adapter_ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/queries")
|
||||||
|
def list_queries():
|
||||||
|
"""List available predefined queries."""
|
||||||
|
adapter = get_adapter()
|
||||||
|
return {
|
||||||
|
"queries": adapter.get_queries()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/navigate")
|
||||||
|
def navigate(query: Optional[str] = None, entity: Optional[str] = None, id: Optional[int] = None):
|
||||||
|
"""
|
||||||
|
Navigate data graph.
|
||||||
|
|
||||||
|
Query mode: ?query=user_with_pets
|
||||||
|
Navigation mode: ?entity=User&id=123
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"nodes": [...],
|
||||||
|
"edges": [...],
|
||||||
|
"summary": {...}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
adapter = get_adapter()
|
||||||
|
result = adapter.navigate(query=query, entity=entity, id=id)
|
||||||
|
return result
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Navigate error: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=int(os.getenv("PORT", "8000")),
|
||||||
|
reload=True,
|
||||||
|
)
|
||||||
4
cfg/amar/link/requirements.txt
Normal file
4
cfg/amar/link/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
psycopg2-binary
|
||||||
|
sqlalchemy
|
||||||
32
cfg/amar/soleprint/.env
Normal file
32
cfg/amar/soleprint/.env
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# SOLEPRINT - Amar Room Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DEPLOYMENT
|
||||||
|
# =============================================================================
|
||||||
|
DEPLOYMENT_NAME=amar_soleprint
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# NETWORK (shared with amar)
|
||||||
|
# =============================================================================
|
||||||
|
NETWORK_NAME=soleprint_network
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PATHS
|
||||||
|
# =============================================================================
|
||||||
|
SOLEPRINT_BARE_PATH=/home/mariano/wdir/spr/gen
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PORTS
|
||||||
|
# =============================================================================
|
||||||
|
SOLEPRINT_PORT=12000
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DATABASE (amar's DB for station tools)
|
||||||
|
# =============================================================================
|
||||||
|
DB_HOST=amar_db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=amarback
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=localdev123
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
# Install system dependencies
|
||||||
libpq-dev gcc \
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
libpq-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
118
cfg/amar/soleprint/README.nginx.md
Normal file
118
cfg/amar/soleprint/README.nginx.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Nginx for Local Development
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `docker-compose.nginx.yml` file provides an **optional** nginx container for local development. This is **NOT** used on AWS (which uses bare metal nginx).
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use nginx container when you want to access services via friendly domains locally:
|
||||||
|
- `http://amarmascotas.local.com` → amar
|
||||||
|
- `http://soleprint.local.com` → soleprint + services
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Generate Nginx Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../ctrl/server
|
||||||
|
./setup.sh --local
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `/tmp/core_room.conf` with your local domain routing.
|
||||||
|
|
||||||
|
### 2. Ensure /etc/hosts is configured
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Should already be there, but verify:
|
||||||
|
grep "127.0.0.1.*amarmascotas.local.com" /etc/hosts
|
||||||
|
grep "127.0.0.1.*soleprint.local.com" /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Stop bare metal nginx (if running)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop nginx
|
||||||
|
# Optional: disable so it doesn't start on boot
|
||||||
|
sudo systemctl disable nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start services WITH nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../ctrl
|
||||||
|
./start.sh --with-nginx
|
||||||
|
# OR manually:
|
||||||
|
cd ../soleprint
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Without Nginx (Default)
|
||||||
|
|
||||||
|
If you don't use nginx, services are accessed via ports:
|
||||||
|
- `http://localhost:3000` → amar frontend
|
||||||
|
- `http://localhost:8000` → amar backend
|
||||||
|
- `http://localhost:13000` → soleprint
|
||||||
|
- `http://localhost:13001` → artery
|
||||||
|
- `http://localhost:13002` → album
|
||||||
|
- `http://localhost:13003` → ward
|
||||||
|
|
||||||
|
The default `docker-compose.yml` exposes these ports, so it works either way.
|
||||||
|
|
||||||
|
## Switching Between Nginx and Direct Access
|
||||||
|
|
||||||
|
**To use nginx routing:**
|
||||||
|
```bash
|
||||||
|
# Stop direct port access
|
||||||
|
cd ../ctrl && ./stop.sh
|
||||||
|
|
||||||
|
# Start with nginx
|
||||||
|
./start.sh --with-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
**To go back to direct ports:**
|
||||||
|
```bash
|
||||||
|
# Stop nginx version
|
||||||
|
cd ../soleprint
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.nginx.yml down
|
||||||
|
|
||||||
|
# Start without nginx
|
||||||
|
cd ../ctrl && ./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## AWS Production
|
||||||
|
|
||||||
|
On AWS, **do NOT use** `docker-compose.nginx.yml`. The AWS setup uses bare metal nginx configured via `ctrl/server/setup.sh --production`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Port 80 already in use:**
|
||||||
|
```bash
|
||||||
|
# Check what's using it
|
||||||
|
ss -tlnp | grep :80
|
||||||
|
|
||||||
|
# If it's nginx bare metal
|
||||||
|
sudo systemctl stop nginx
|
||||||
|
|
||||||
|
# If it's another container
|
||||||
|
docker ps | grep :80
|
||||||
|
docker stop <container_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config not found error:**
|
||||||
|
```bash
|
||||||
|
# Make sure you ran setup first
|
||||||
|
cd ../ctrl/server && ./setup.sh --local
|
||||||
|
|
||||||
|
# Verify config exists
|
||||||
|
ls -la /tmp/core_room.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
**DNS not resolving:**
|
||||||
|
```bash
|
||||||
|
# Check /etc/hosts
|
||||||
|
cat /etc/hosts | grep local.com
|
||||||
|
|
||||||
|
# Test DNS
|
||||||
|
ping -c 1 amarmascotas.local.com
|
||||||
|
```
|
||||||
13
cfg/amar/soleprint/artery/shunts/amar/.env.example
Normal file
13
cfg/amar/soleprint/artery/shunts/amar/.env.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Amar (MOCK) Vein Configuration
|
||||||
|
API_PORT=8005
|
||||||
|
|
||||||
|
# Mock data settings
|
||||||
|
MOCK_DATA_PATH=./mock_data
|
||||||
|
|
||||||
|
# Mock behavior
|
||||||
|
ENABLE_RANDOM_DELAYS=true
|
||||||
|
MIN_DELAY_MS=100
|
||||||
|
MAX_DELAY_MS=500
|
||||||
|
|
||||||
|
# Simulate errors
|
||||||
|
ERROR_RATE=0.0 # 0.0 to 1.0 (0% to 100%)
|
||||||
173
cfg/amar/soleprint/artery/shunts/amar/README.md
Normal file
173
cfg/amar/soleprint/artery/shunts/amar/README.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Amar (MOCK) Vein
|
||||||
|
|
||||||
|
Mock Amar API for testing - returns predictable test data for turnero flow testing.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Enables testing of the turnero flow (VET-535-540) without hitting the real Amar backend:
|
||||||
|
- Guest petowner creation (VET-536)
|
||||||
|
- Pet creation (VET-537)
|
||||||
|
- Cart creation and price calculation (VET-538)
|
||||||
|
- Service/category filtering (VET-539, VET-540)
|
||||||
|
- Service request creation
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Start the mock server
|
||||||
|
python run.py
|
||||||
|
|
||||||
|
# API docs: http://localhost:8005/docs
|
||||||
|
# Health check: http://localhost:8005/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and adjust:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API_PORT=8005 # Server port
|
||||||
|
ENABLE_RANDOM_DELAYS=true # Add realistic delays
|
||||||
|
MIN_DELAY_MS=100 # Minimum delay
|
||||||
|
MAX_DELAY_MS=500 # Maximum delay
|
||||||
|
ERROR_RATE=0.0 # Error rate (0.0 to 1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Turnero Flow (VET-536-540)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create guest petowner (VET-536)
|
||||||
|
POST /api/v1/pet-owners/
|
||||||
|
{
|
||||||
|
"address": "Av. Santa Fe 1234, Palermo"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create pet (VET-537)
|
||||||
|
POST /api/v1/pets/
|
||||||
|
{
|
||||||
|
"owner_id": 1234,
|
||||||
|
"name": "Luna",
|
||||||
|
"species": "DOG",
|
||||||
|
"age": 3,
|
||||||
|
"age_unit": "years"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create cart (VET-538)
|
||||||
|
POST /api/v1/cart/
|
||||||
|
{
|
||||||
|
"owner_id": 1234
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add item to cart
|
||||||
|
POST /api/v1/cart/{cart_id}/items/
|
||||||
|
{
|
||||||
|
"service_id": 1,
|
||||||
|
"pet_id": 5678,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# List services (VET-540)
|
||||||
|
GET /api/v1/services/?species=DOG&neighborhood_id=1
|
||||||
|
|
||||||
|
# List categories (VET-539)
|
||||||
|
GET /api/v1/categories/?species=DOG&neighborhood_id=1
|
||||||
|
|
||||||
|
# Create service request
|
||||||
|
POST /solicitudes/service-requests/?cart_id=12345
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mock Control
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get mock database stats
|
||||||
|
GET /mock/stats
|
||||||
|
|
||||||
|
# Reset mock database
|
||||||
|
GET /mock/reset
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
All responses include `_mock: true` to identify mock data:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1234,
|
||||||
|
"address": "Av. Santa Fe 1234, Palermo",
|
||||||
|
"is_guest": true,
|
||||||
|
"_mock": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Complete Turnero Flow
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8005"
|
||||||
|
|
||||||
|
# Step 1: Create guest petowner
|
||||||
|
owner_resp = requests.post(f"{BASE_URL}/api/v1/pet-owners/", json={
|
||||||
|
"address": "Av. Santa Fe 1234, Palermo"
|
||||||
|
})
|
||||||
|
owner = owner_resp.json()
|
||||||
|
|
||||||
|
# Step 2: Create pet
|
||||||
|
pet_resp = requests.post(f"{BASE_URL}/api/v1/pets/", json={
|
||||||
|
"owner_id": owner["id"],
|
||||||
|
"name": "Luna",
|
||||||
|
"species": "DOG",
|
||||||
|
"age": 3,
|
||||||
|
"age_unit": "years"
|
||||||
|
})
|
||||||
|
pet = pet_resp.json()
|
||||||
|
|
||||||
|
# Step 3: Create cart
|
||||||
|
cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/", json={
|
||||||
|
"owner_id": owner["id"]
|
||||||
|
})
|
||||||
|
cart = cart_resp.json()
|
||||||
|
|
||||||
|
# Step 4: Get available services
|
||||||
|
services_resp = requests.get(
|
||||||
|
f"{BASE_URL}/api/v1/services/",
|
||||||
|
params={"species": "DOG", "neighborhood_id": owner["neighborhood"]["id"]}
|
||||||
|
)
|
||||||
|
services = services_resp.json()
|
||||||
|
|
||||||
|
# Step 5: Add service to cart
|
||||||
|
cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/{cart['id']}/items/", json={
|
||||||
|
"service_id": services[0]["id"],
|
||||||
|
"pet_id": pet["id"],
|
||||||
|
"quantity": 1
|
||||||
|
})
|
||||||
|
cart = cart_resp.json()
|
||||||
|
print(f"Cart total: ${cart['resume']['total']}")
|
||||||
|
|
||||||
|
# Step 6: Create service request
|
||||||
|
request_resp = requests.post(
|
||||||
|
f"{BASE_URL}/solicitudes/service-requests/",
|
||||||
|
params={"cart_id": cart["id"]}
|
||||||
|
)
|
||||||
|
service_request = request_resp.json()
|
||||||
|
print(f"Service request created: {service_request['id']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Generator
|
||||||
|
|
||||||
|
This vein uses the independent `datagen` tool from `ward/tools/datagen/amar.py`.
|
||||||
|
See `ward/tools/datagen/README.md` for data generation details.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Mock database is in-memory (resets on server restart)
|
||||||
|
- Use `/mock/reset` to clear data during testing
|
||||||
|
- All IDs are randomly generated on creation
|
||||||
|
- Multi-pet discounts are automatically calculated
|
||||||
1
cfg/amar/soleprint/artery/shunts/amar/__init__.py
Normal file
1
cfg/amar/soleprint/artery/shunts/amar/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Amar (MOCK) vein - Mock Amar API for testing."""
|
||||||
1
cfg/amar/soleprint/artery/shunts/amar/api/__init__.py
Normal file
1
cfg/amar/soleprint/artery/shunts/amar/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""API routes for Amar mock vein."""
|
||||||
302
cfg/amar/soleprint/artery/shunts/amar/api/routes.py
Normal file
302
cfg/amar/soleprint/artery/shunts/amar/api/routes.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
"""API routes for Amar (MOCK) vein - Mock Amar API for testing."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# Import datagen from ward/tools
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
ward_tools_path = Path(__file__).parent.parent.parent.parent.parent / "ward" / "tools"
|
||||||
|
sys.path.insert(0, str(ward_tools_path))
|
||||||
|
|
||||||
|
from datagen.amar import AmarDataGenerator
|
||||||
|
|
||||||
|
from ..core.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# In-memory storage for mock data (reset on restart)
|
||||||
|
MOCK_DB = {
|
||||||
|
"petowners": {},
|
||||||
|
"pets": {},
|
||||||
|
"carts": {},
|
||||||
|
"service_requests": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Request/Response Models
|
||||||
|
class CreatePetOwnerRequest(BaseModel):
|
||||||
|
address: str
|
||||||
|
email: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePetRequest(BaseModel):
|
||||||
|
owner_id: int
|
||||||
|
name: str
|
||||||
|
species: str # DOG, CAT
|
||||||
|
age: Optional[int] = None
|
||||||
|
age_unit: str = "years" # years, months
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCartRequest(BaseModel):
|
||||||
|
owner_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class AddCartItemRequest(BaseModel):
|
||||||
|
service_id: int
|
||||||
|
pet_id: int
|
||||||
|
quantity: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def _mock_delay():
|
||||||
|
"""Add realistic delay if enabled."""
|
||||||
|
if settings.enable_random_delays:
|
||||||
|
delay_ms = random.randint(settings.min_delay_ms, settings.max_delay_ms)
|
||||||
|
await asyncio.sleep(delay_ms / 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_error():
|
||||||
|
"""Randomly raise an error based on error_rate."""
|
||||||
|
if random.random() < settings.error_rate:
|
||||||
|
raise HTTPException(500, "Mock error: Simulated failure")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health():
|
||||||
|
"""Health check endpoint."""
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"vein": "Amar\n(MOCK)",
|
||||||
|
"message": "Mock Amar API for testing",
|
||||||
|
"_mock": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/pet-owners/")
|
||||||
|
async def create_petowner(request: CreatePetOwnerRequest):
|
||||||
|
"""Create a guest petowner (VET-536)."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
owner = AmarDataGenerator.petowner(
|
||||||
|
address=request.address,
|
||||||
|
is_guest=True,
|
||||||
|
email=request.email,
|
||||||
|
phone=request.phone,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store in mock DB
|
||||||
|
MOCK_DB["petowners"][owner["id"]] = owner
|
||||||
|
|
||||||
|
owner["_mock"] = True
|
||||||
|
return owner
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/pet-owners/{owner_id}")
|
||||||
|
async def get_petowner(owner_id: int):
|
||||||
|
"""Get petowner by ID."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
owner = MOCK_DB["petowners"].get(owner_id)
|
||||||
|
if not owner:
|
||||||
|
raise HTTPException(404, f"PetOwner {owner_id} not found")
|
||||||
|
|
||||||
|
return owner
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/pets/")
|
||||||
|
async def create_pet(request: CreatePetRequest):
|
||||||
|
"""Create a pet (VET-537)."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
# Verify owner exists
|
||||||
|
if request.owner_id not in MOCK_DB["petowners"]:
|
||||||
|
raise HTTPException(404, f"Owner {request.owner_id} not found")
|
||||||
|
|
||||||
|
pet = AmarDataGenerator.pet(
|
||||||
|
owner_id=request.owner_id,
|
||||||
|
name=request.name,
|
||||||
|
species=request.species,
|
||||||
|
age_value=request.age,
|
||||||
|
age_unit=request.age_unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store in mock DB
|
||||||
|
MOCK_DB["pets"][pet["id"]] = pet
|
||||||
|
|
||||||
|
pet["_mock"] = True
|
||||||
|
return pet
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/pets/{pet_id}")
|
||||||
|
async def get_pet(pet_id: int):
|
||||||
|
"""Get pet by ID."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
pet = MOCK_DB["pets"].get(pet_id)
|
||||||
|
if not pet:
|
||||||
|
raise HTTPException(404, f"Pet {pet_id} not found")
|
||||||
|
|
||||||
|
return pet
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/cart/")
|
||||||
|
async def create_cart(request: CreateCartRequest):
|
||||||
|
"""Create a cart (VET-538)."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
# Verify owner exists
|
||||||
|
if request.owner_id not in MOCK_DB["petowners"]:
|
||||||
|
raise HTTPException(404, f"Owner {request.owner_id} not found")
|
||||||
|
|
||||||
|
cart = AmarDataGenerator.cart(owner_id=request.owner_id)
|
||||||
|
|
||||||
|
# Store in mock DB
|
||||||
|
MOCK_DB["carts"][cart["id"]] = cart
|
||||||
|
|
||||||
|
cart["_mock"] = True
|
||||||
|
return cart
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/cart/{cart_id}/items/")
|
||||||
|
async def add_cart_item(cart_id: int, request: AddCartItemRequest):
|
||||||
|
"""Add item to cart and recalculate summary."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
cart = MOCK_DB["carts"].get(cart_id)
|
||||||
|
if not cart:
|
||||||
|
raise HTTPException(404, f"Cart {cart_id} not found")
|
||||||
|
|
||||||
|
# Get service price
|
||||||
|
services = AmarDataGenerator.SERVICES
|
||||||
|
service = next((s for s in services if s["id"] == request.service_id), None)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(404, f"Service {request.service_id} not found")
|
||||||
|
|
||||||
|
# Add item
|
||||||
|
item = {
|
||||||
|
"service_id": request.service_id,
|
||||||
|
"service_name": service["name"],
|
||||||
|
"pet_id": request.pet_id,
|
||||||
|
"quantity": request.quantity,
|
||||||
|
"price": service["price"],
|
||||||
|
}
|
||||||
|
|
||||||
|
cart["items"].append(item)
|
||||||
|
|
||||||
|
# Recalculate summary
|
||||||
|
cart = AmarDataGenerator.calculate_cart_summary(cart, cart["items"])
|
||||||
|
MOCK_DB["carts"][cart_id] = cart
|
||||||
|
|
||||||
|
cart["_mock"] = True
|
||||||
|
return cart
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/cart/{cart_id}")
|
||||||
|
async def get_cart(cart_id: int):
|
||||||
|
"""Get cart by ID."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
cart = MOCK_DB["carts"].get(cart_id)
|
||||||
|
if not cart:
|
||||||
|
raise HTTPException(404, f"Cart {cart_id} not found")
|
||||||
|
|
||||||
|
return cart
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/services/")
|
||||||
|
async def list_services(
|
||||||
|
species: Optional[str] = Query(None),
|
||||||
|
neighborhood_id: Optional[int] = Query(None),
|
||||||
|
):
|
||||||
|
"""List available services filtered by species and neighborhood (VET-540)."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
services = AmarDataGenerator.filter_services(
|
||||||
|
species=species,
|
||||||
|
neighborhood_id=neighborhood_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for service in services:
|
||||||
|
service["_mock"] = True
|
||||||
|
|
||||||
|
return services
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/categories/")
|
||||||
|
async def list_categories(
|
||||||
|
species: Optional[str] = Query(None),
|
||||||
|
neighborhood_id: Optional[int] = Query(None),
|
||||||
|
):
|
||||||
|
"""List categories with available services (VET-539)."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
categories = AmarDataGenerator.filter_categories(
|
||||||
|
species=species,
|
||||||
|
neighborhood_id=neighborhood_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for category in categories:
|
||||||
|
category["_mock"] = True
|
||||||
|
|
||||||
|
return categories
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/solicitudes/service-requests/")
|
||||||
|
async def create_service_request(cart_id: int, requested_date: Optional[str] = None):
|
||||||
|
"""Create a service request."""
|
||||||
|
await _mock_delay()
|
||||||
|
_maybe_error()
|
||||||
|
|
||||||
|
cart = MOCK_DB["carts"].get(cart_id)
|
||||||
|
if not cart:
|
||||||
|
raise HTTPException(404, f"Cart {cart_id} not found")
|
||||||
|
|
||||||
|
request = AmarDataGenerator.service_request(
|
||||||
|
cart_id=cart_id,
|
||||||
|
requested_date=requested_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
MOCK_DB["service_requests"][request["id"]] = request
|
||||||
|
|
||||||
|
request["_mock"] = True
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mock/reset")
|
||||||
|
async def reset_mock_db():
|
||||||
|
"""Reset the mock database (useful for testing)."""
|
||||||
|
MOCK_DB["petowners"].clear()
|
||||||
|
MOCK_DB["pets"].clear()
|
||||||
|
MOCK_DB["carts"].clear()
|
||||||
|
MOCK_DB["service_requests"].clear()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Mock database reset",
|
||||||
|
"_mock": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mock/stats")
|
||||||
|
async def mock_stats():
|
||||||
|
"""Get mock database statistics."""
|
||||||
|
return {
|
||||||
|
"petowners": len(MOCK_DB["petowners"]),
|
||||||
|
"pets": len(MOCK_DB["pets"]),
|
||||||
|
"carts": len(MOCK_DB["carts"]),
|
||||||
|
"service_requests": len(MOCK_DB["service_requests"]),
|
||||||
|
"_mock": True,
|
||||||
|
}
|
||||||
1
cfg/amar/soleprint/artery/shunts/amar/core/__init__.py
Normal file
1
cfg/amar/soleprint/artery/shunts/amar/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Core logic for Amar mock API."""
|
||||||
27
cfg/amar/soleprint/artery/shunts/amar/core/config.py
Normal file
27
cfg/amar/soleprint/artery/shunts/amar/core/config.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""Configuration for Amar mock vein."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
ENV_FILE = Path(__file__).parent.parent / ".env"
|
||||||
|
|
||||||
|
|
||||||
|
class AmarMockConfig(BaseSettings):
|
||||||
|
"""Configuration for Amar (MOCK) vein."""
|
||||||
|
|
||||||
|
api_port: int = 8005
|
||||||
|
mock_data_path: str = "./mock_data"
|
||||||
|
|
||||||
|
# Mock behavior
|
||||||
|
enable_random_delays: bool = True
|
||||||
|
min_delay_ms: int = 100
|
||||||
|
max_delay_ms: int = 500
|
||||||
|
error_rate: float = 0.0 # 0.0 to 1.0
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"env_file": ENV_FILE if ENV_FILE.exists() else None,
|
||||||
|
"env_file_encoding": "utf-8",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
settings = AmarMockConfig()
|
||||||
40
cfg/amar/soleprint/artery/shunts/amar/main.py
Normal file
40
cfg/amar/soleprint/artery/shunts/amar/main.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Amar (MOCK) Vein - FastAPI app."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from .api.routes import router
|
||||||
|
from .core.config import settings
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Amar API (MOCK)",
|
||||||
|
description="Mock Amar API for testing - returns predictable test data",
|
||||||
|
version="0.1.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable CORS for testing from frontend
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # In production, specify exact origins
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Templates for configuration UI
|
||||||
|
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
def index(request: Request):
|
||||||
|
"""Mock configuration UI."""
|
||||||
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
|
# Include router at root (matches real Amar API structure)
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=settings.api_port)
|
||||||
@@ -2,4 +2,3 @@ fastapi>=0.104.0
|
|||||||
uvicorn>=0.24.0
|
uvicorn>=0.24.0
|
||||||
pydantic>=2.0.0
|
pydantic>=2.0.0
|
||||||
pydantic-settings>=2.0.0
|
pydantic-settings>=2.0.0
|
||||||
httpx>=0.25.0
|
|
||||||
18
cfg/amar/soleprint/artery/shunts/amar/run.py
Normal file
18
cfg/amar/soleprint/artery/shunts/amar/run.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Standalone runner for Amar mock vein."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import uvicorn
|
||||||
|
from main import app
|
||||||
|
from core.config import settings
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logger.info(f"Starting Amar (MOCK) vein on port {settings.api_port}")
|
||||||
|
logger.info(f"API docs: http://localhost:{settings.api_port}/docs")
|
||||||
|
logger.info(f"Health check: http://localhost:{settings.api_port}/health")
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=settings.api_port, reload=True)
|
||||||
298
cfg/amar/soleprint/artery/shunts/amar/templates/index.html
Normal file
298
cfg/amar/soleprint/artery/shunts/amar/templates/index.html
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Amar API (MOCK) - Configuration</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #111827;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
header {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: #111827;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
|
||||||
|
.subtitle { opacity: 0.8; font-size: 0.875rem; }
|
||||||
|
.mock-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #111827;
|
||||||
|
color: #f59e0b;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #1f2937;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
.endpoint-list { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.endpoint-card {
|
||||||
|
background: #374151;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.endpoint-card:hover { border-color: #f59e0b; background: #4b5563; }
|
||||||
|
.endpoint-card.active { border-color: #f59e0b; background: #4b5563; }
|
||||||
|
.endpoint-method {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.method-post { background: #10b981; color: white; }
|
||||||
|
.method-get { background: #3b82f6; color: white; }
|
||||||
|
.endpoint-path { font-family: monospace; font-size: 0.875rem; }
|
||||||
|
.endpoint-desc { font-size: 0.75rem; color: #9ca3af; margin-top: 6px; }
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
.form-input, .form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #374151;
|
||||||
|
border: 1px solid #4b5563;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.form-textarea { min-height: 200px; font-family: monospace; }
|
||||||
|
.form-input:focus, .form-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #d97706; }
|
||||||
|
.btn-secondary {
|
||||||
|
background: #4b5563;
|
||||||
|
color: #e5e7eb;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: #6b7280; }
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: #374151;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.stat-value { font-size: 1.5rem; font-weight: 600; color: #f59e0b; }
|
||||||
|
.stat-label { font-size: 0.75rem; color: #9ca3af; margin-top: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Amar API <span class="mock-badge">MOCK</span></h1>
|
||||||
|
<div class="subtitle">Configure mock responses for Amar backend endpoints</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">Mock Database Stats</div>
|
||||||
|
<div class="stats" id="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">-</div>
|
||||||
|
<div class="stat-label">Pet Owners</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">-</div>
|
||||||
|
<div class="stat-label">Pets</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">-</div>
|
||||||
|
<div class="stat-label">Carts</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">-</div>
|
||||||
|
<div class="stat-label">Requests</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<button class="btn btn-secondary" onclick="resetMock()">Reset Database</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Endpoint Configuration -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">Configure Endpoint Responses</div>
|
||||||
|
<div class="endpoint-list">
|
||||||
|
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/pet-owners/', 'petowner')">
|
||||||
|
<div>
|
||||||
|
<span class="endpoint-method method-post">POST</span>
|
||||||
|
<span class="endpoint-path">/api/v1/pet-owners/</span>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-desc">Create guest pet owner (VET-536)</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/pets/', 'pet')">
|
||||||
|
<div>
|
||||||
|
<span class="endpoint-method method-post">POST</span>
|
||||||
|
<span class="endpoint-path">/api/v1/pets/</span>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-desc">Create pet (VET-537)</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/cart/', 'cart')">
|
||||||
|
<div>
|
||||||
|
<span class="endpoint-method method-post">POST</span>
|
||||||
|
<span class="endpoint-path">/api/v1/cart/</span>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-desc">Create cart (VET-538)</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-card" onclick="selectEndpoint('GET', '/api/v1/services/', 'services')">
|
||||||
|
<div>
|
||||||
|
<span class="endpoint-method method-get">GET</span>
|
||||||
|
<span class="endpoint-path">/api/v1/services/</span>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-desc">List services (VET-540)</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-card" onclick="selectEndpoint('GET', '/api/v1/categories/', 'categories')">
|
||||||
|
<div>
|
||||||
|
<span class="endpoint-method method-get">GET</span>
|
||||||
|
<span class="endpoint-path">/api/v1/categories/</span>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint-desc">List categories (VET-539)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Response Editor -->
|
||||||
|
<div class="section" id="responseEditor" style="display: none;">
|
||||||
|
<div class="section-header">Edit Response</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Endpoint</label>
|
||||||
|
<input class="form-input" id="endpointDisplay" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Mock Response (JSON)</label>
|
||||||
|
<textarea class="form-textarea" id="responseJson" placeholder='{"id": 123, "name": "Luna", "_mock": true}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">HTTP Status Code</label>
|
||||||
|
<input type="number" class="form-input" id="statusCode" value="200">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Delay (ms)</label>
|
||||||
|
<input type="number" class="form-input" id="delay" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" onclick="saveResponse()">Save Response</button>
|
||||||
|
<button class="btn btn-secondary" onclick="closeEditor()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Test -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">Quick Test</div>
|
||||||
|
<p style="color: #9ca3af; margin-bottom: 12px;">Test endpoint URL to hit for configured responses:</p>
|
||||||
|
<div class="form-input" style="background: #374151; user-select: all;">
|
||||||
|
http://localhost:8005/api/v1/pet-owners/
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let selectedEndpoint = null;
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/mock/stats');
|
||||||
|
const data = await resp.json();
|
||||||
|
document.getElementById('stats').innerHTML = `
|
||||||
|
<div class="stat-card"><div class="stat-value">${data.pet_owners || 0}</div><div class="stat-label">Pet Owners</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">${data.pets || 0}</div><div class="stat-label">Pets</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">${data.carts || 0}</div><div class="stat-label">Carts</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">${data.service_requests || 0}</div><div class="stat-label">Requests</div></div>
|
||||||
|
`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load stats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetMock() {
|
||||||
|
if (confirm('Reset all mock data?')) {
|
||||||
|
await fetch('/mock/reset');
|
||||||
|
loadStats();
|
||||||
|
alert('Mock database reset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEndpoint(method, path, type) {
|
||||||
|
selectedEndpoint = {method, path, type};
|
||||||
|
document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('active'));
|
||||||
|
event.currentTarget.classList.add('active');
|
||||||
|
document.getElementById('responseEditor').style.display = 'block';
|
||||||
|
document.getElementById('endpointDisplay').value = `${method} ${path}`;
|
||||||
|
document.getElementById('responseJson').value = getDefaultResponse(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultResponse(type) {
|
||||||
|
const defaults = {
|
||||||
|
petowner: JSON.stringify({"id": 1234, "address": "Av Santa Fe 1234", "is_guest": true, "_mock": true}, null, 2),
|
||||||
|
pet: JSON.stringify({"id": 5678, "name": "Luna", "species": "DOG", "age": 3, "_mock": true}, null, 2),
|
||||||
|
cart: JSON.stringify({"id": 9012, "items": [], "total": 0, "_mock": true}, null, 2),
|
||||||
|
services: JSON.stringify([{"id": 1, "name": "Vacunación", "price": 1500, "_mock": true}], null, 2),
|
||||||
|
categories: JSON.stringify([{"id": 1, "name": "Salud", "services": 5, "_mock": true}], null, 2)
|
||||||
|
};
|
||||||
|
return defaults[type] || '{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveResponse() {
|
||||||
|
alert('Mock response saved (feature pending implementation)');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditor() {
|
||||||
|
document.getElementById('responseEditor').style.display = 'none';
|
||||||
|
selectedEndpoint = null;
|
||||||
|
document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('active'));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStats();
|
||||||
|
setInterval(loadStats, 5000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
184
cfg/amar/soleprint/atlas/books/arch/01-backend-architecture.dot
Normal file
184
cfg/amar/soleprint/atlas/books/arch/01-backend-architecture.dot
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
digraph BackendArchitecture {
|
||||||
|
// Graph settings
|
||||||
|
rankdir=TB
|
||||||
|
compound=true
|
||||||
|
splines=ortho
|
||||||
|
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11]
|
||||||
|
edge [fontname="Helvetica", fontsize=9]
|
||||||
|
|
||||||
|
// Color scheme
|
||||||
|
// Django Core: #092E20 (dark green)
|
||||||
|
// Mascotas: #4A90D9 (blue)
|
||||||
|
// Productos: #50C878 (green)
|
||||||
|
// Solicitudes: #FF6B6B (coral)
|
||||||
|
// Common: #9B59B6 (purple)
|
||||||
|
// External: #F39C12 (orange)
|
||||||
|
// Payments: #E74C3C (red)
|
||||||
|
|
||||||
|
label="AMAR Mascotas - Backend Architecture (Django)\n\n"
|
||||||
|
labelloc="t"
|
||||||
|
fontsize=16
|
||||||
|
fontname="Helvetica-Bold"
|
||||||
|
|
||||||
|
// Django Core Cluster
|
||||||
|
subgraph cluster_django_core {
|
||||||
|
label="Django Core"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E8F5E9"
|
||||||
|
color="#2E7D32"
|
||||||
|
|
||||||
|
auth_user [label="auth.User\n(Django Auth)", fillcolor="#C8E6C9"]
|
||||||
|
django_admin [label="Django Admin\nInterface", fillcolor="#C8E6C9"]
|
||||||
|
drf [label="Django REST\nFramework", fillcolor="#C8E6C9"]
|
||||||
|
jwt_auth [label="JWT Authentication\n(SimpleJWT)", fillcolor="#C8E6C9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mascotas App Cluster
|
||||||
|
subgraph cluster_mascotas {
|
||||||
|
label="mascotas (Pets & Veterinarians)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E3F2FD"
|
||||||
|
color="#1565C0"
|
||||||
|
|
||||||
|
petowner [label="PetOwner\n(Cliente/Tutor)", fillcolor="#BBDEFB"]
|
||||||
|
pet [label="Pet\n(Mascota)", fillcolor="#BBDEFB"]
|
||||||
|
veterinarian [label="Veterinarian\n(Profesional)", fillcolor="#BBDEFB"]
|
||||||
|
vetvisit [label="VetVisit\n(Consulta)", fillcolor="#BBDEFB"]
|
||||||
|
vetvisitreport [label="VetVisitReport\n(Informe Clinico)", fillcolor="#BBDEFB"]
|
||||||
|
availability [label="Availability /\nUnavailability", fillcolor="#BBDEFB"]
|
||||||
|
pet_health [label="PetVaccine /\nPetStudy", fillcolor="#BBDEFB"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Productos App Cluster
|
||||||
|
subgraph cluster_productos {
|
||||||
|
label="productos (Services & Pricing)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E8F5E9"
|
||||||
|
color="#2E7D32"
|
||||||
|
|
||||||
|
service [label="Service\n(Servicio)", fillcolor="#C8E6C9"]
|
||||||
|
category [label="Category / Group\n(Categorias)", fillcolor="#C8E6C9"]
|
||||||
|
prices [label="Prices\n(Precios)", fillcolor="#C8E6C9"]
|
||||||
|
discounts [label="Discounts\n(Descuentos)", fillcolor="#C8E6C9"]
|
||||||
|
cart [label="Cart / CartItem\n(Carrito)", fillcolor="#C8E6C9"]
|
||||||
|
combo [label="ServiceCombo\n(Paquetes)", fillcolor="#C8E6C9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solicitudes App Cluster
|
||||||
|
subgraph cluster_solicitudes {
|
||||||
|
label="solicitudes (Service Requests)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFEBEE"
|
||||||
|
color="#C62828"
|
||||||
|
|
||||||
|
servicerequest [label="ServiceRequest\n(Solicitud)", fillcolor="#FFCDD2"]
|
||||||
|
statehistory [label="StateHistory\n(Historial)", fillcolor="#FFCDD2"]
|
||||||
|
vetasked [label="VeterinarianAsked\n(Vet Consultado)", fillcolor="#FFCDD2"]
|
||||||
|
reminders [label="Reminders\n(Recordatorios)", fillcolor="#FFCDD2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common App Cluster
|
||||||
|
subgraph cluster_common {
|
||||||
|
label="common (Shared Models)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#F3E5F5"
|
||||||
|
color="#7B1FA2"
|
||||||
|
|
||||||
|
campaign [label="Campaign\n(Marketing)", fillcolor="#E1BEE7"]
|
||||||
|
tag [label="Tag\n(Etiquetas)", fillcolor="#E1BEE7"]
|
||||||
|
specialty [label="Specialty\n(Especialidades)", fillcolor="#E1BEE7"]
|
||||||
|
medication [label="Medication\n(Medicamentos)", fillcolor="#E1BEE7"]
|
||||||
|
breed [label="PetBreed / Vaccine\n/ Study", fillcolor="#E1BEE7"]
|
||||||
|
neighborhood [label="Neighborhood /\nProvince / Locality", fillcolor="#E1BEE7"]
|
||||||
|
turnfee [label="IndividualTurnFee\nGroup", fillcolor="#E1BEE7"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payments App Cluster
|
||||||
|
subgraph cluster_payments {
|
||||||
|
label="payments (Payment Processing)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FCE4EC"
|
||||||
|
color="#AD1457"
|
||||||
|
|
||||||
|
mercadopago [label="MercadoPago\nAccount", fillcolor="#F8BBD9"]
|
||||||
|
mpnotification [label="MP Notification\n(Webhooks)", fillcolor="#F8BBD9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// External Integrations Cluster
|
||||||
|
subgraph cluster_external {
|
||||||
|
label="External Integrations"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFF3E0"
|
||||||
|
color="#E65100"
|
||||||
|
|
||||||
|
google_cal [label="Google Calendar\n(Agenda)", fillcolor="#FFE0B2"]
|
||||||
|
google_sheets [label="Google Sheets\n(Exports)", fillcolor="#FFE0B2"]
|
||||||
|
mercately [label="Mercately\n(WhatsApp)", fillcolor="#FFE0B2"]
|
||||||
|
afip [label="AFIP\n(Facturacion)", fillcolor="#FFE0B2"]
|
||||||
|
celery [label="Celery\n(Async Tasks)", fillcolor="#FFE0B2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AFIP Integration
|
||||||
|
subgraph cluster_afip {
|
||||||
|
label="django_afip (Invoicing)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFFDE7"
|
||||||
|
color="#F9A825"
|
||||||
|
|
||||||
|
receipt [label="Receipt\n(Comprobante)", fillcolor="#FFF9C4"]
|
||||||
|
taxpayer [label="TaxPayer\n(Contribuyente)", fillcolor="#FFF9C4"]
|
||||||
|
pos [label="PointOfSales\n(Punto de Venta)", fillcolor="#FFF9C4"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relationships - Core
|
||||||
|
auth_user -> petowner [label="1:1 optional", color="#666"]
|
||||||
|
auth_user -> veterinarian [label="1:1", color="#666"]
|
||||||
|
drf -> jwt_auth [style=dashed, color="#999"]
|
||||||
|
|
||||||
|
// Relationships - Mascotas
|
||||||
|
petowner -> pet [label="1:N", color="#1565C0"]
|
||||||
|
petowner -> servicerequest [label="1:N", color="#1565C0"]
|
||||||
|
pet -> vetvisit [label="N:M", color="#1565C0"]
|
||||||
|
pet -> pet_health [label="1:N", color="#1565C0"]
|
||||||
|
veterinarian -> vetvisit [label="1:N", color="#1565C0"]
|
||||||
|
veterinarian -> availability [label="1:N", color="#1565C0"]
|
||||||
|
vetvisit -> vetvisitreport [label="1:N", color="#1565C0"]
|
||||||
|
vetvisit -> servicerequest [label="1:1", style=dashed, color="#1565C0"]
|
||||||
|
|
||||||
|
// Relationships - Productos
|
||||||
|
service -> category [label="N:1", color="#2E7D32"]
|
||||||
|
service -> prices [label="1:N", color="#2E7D32"]
|
||||||
|
service -> discounts [label="1:N", color="#2E7D32"]
|
||||||
|
cart -> petowner [label="N:1", color="#2E7D32"]
|
||||||
|
cart -> veterinarian [label="N:1 optional", color="#2E7D32", style=dashed]
|
||||||
|
service -> cart [label="via CartItem", color="#2E7D32"]
|
||||||
|
combo -> service [label="contains", color="#2E7D32"]
|
||||||
|
|
||||||
|
// Relationships - Solicitudes
|
||||||
|
servicerequest -> cart [label="1:1", color="#C62828"]
|
||||||
|
servicerequest -> statehistory [label="1:N", color="#C62828"]
|
||||||
|
servicerequest -> vetasked [label="1:N", color="#C62828"]
|
||||||
|
vetasked -> reminders [label="1:N", color="#C62828"]
|
||||||
|
veterinarian -> vetasked [label="N:1", color="#C62828"]
|
||||||
|
|
||||||
|
// Relationships - Common
|
||||||
|
petowner -> neighborhood [label="N:1", color="#7B1FA2"]
|
||||||
|
veterinarian -> specialty [label="N:M", color="#7B1FA2"]
|
||||||
|
veterinarian -> neighborhood [label="N:M coverage", color="#7B1FA2"]
|
||||||
|
servicerequest -> campaign [label="N:1 optional", color="#7B1FA2", style=dashed]
|
||||||
|
servicerequest -> tag [label="N:M", color="#7B1FA2"]
|
||||||
|
vetvisitreport -> medication [label="references", color="#7B1FA2", style=dashed]
|
||||||
|
veterinarian -> turnfee [label="N:M", color="#7B1FA2"]
|
||||||
|
|
||||||
|
// Relationships - Payments & External
|
||||||
|
servicerequest -> mercadopago [label="payment", color="#AD1457"]
|
||||||
|
mpnotification -> servicerequest [label="confirms", color="#AD1457"]
|
||||||
|
vetvisit -> google_cal [label="sync", color="#E65100", style=dashed]
|
||||||
|
servicerequest -> mercately [label="notify", color="#E65100", style=dashed]
|
||||||
|
reminders -> celery [label="scheduled", color="#E65100", style=dashed]
|
||||||
|
|
||||||
|
// AFIP relationships
|
||||||
|
vetvisit -> receipt [label="1:1 optional", color="#F9A825", style=dashed]
|
||||||
|
receipt -> taxpayer [label="N:1", color="#F9A825"]
|
||||||
|
receipt -> pos [label="N:1", color="#F9A825"]
|
||||||
|
}
|
||||||
585
cfg/amar/soleprint/atlas/books/arch/01-backend-architecture.svg
Normal file
585
cfg/amar/soleprint/atlas/books/arch/01-backend-architecture.svg
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.0.5 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: BackendArchitecture Pages: 1 -->
|
||||||
|
<svg width="1559pt" height="935pt"
|
||||||
|
viewBox="0.00 0.00 1559.00 935.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 931.4)">
|
||||||
|
<title>BackendArchitecture</title>
|
||||||
|
<polygon fill="white" stroke="none" points="-4,4 -4,-931.4 1555,-931.4 1555,4 -4,4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="775.5" y="-908.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas - Backend Architecture (Django)</text>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_django_core</title>
|
||||||
|
<path fill="#e8f5e9" stroke="#2e7d32" d="M169,-496.4C169,-496.4 449,-496.4 449,-496.4 455,-496.4 461,-502.4 461,-508.4 461,-508.4 461,-726 461,-726 461,-732 455,-738 449,-738 449,-738 169,-738 169,-738 163,-738 157,-732 157,-726 157,-726 157,-508.4 157,-508.4 157,-502.4 163,-496.4 169,-496.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="309" y="-718.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Django Core</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_mascotas</title>
|
||||||
|
<path fill="#e3f2fd" stroke="#1565c0" d="M481,-143.2C481,-143.2 733,-143.2 733,-143.2 739,-143.2 745,-149.2 745,-155.2 745,-155.2 745,-590.8 745,-590.8 745,-596.8 739,-602.8 733,-602.8 733,-602.8 481,-602.8 481,-602.8 475,-602.8 469,-596.8 469,-590.8 469,-590.8 469,-155.2 469,-155.2 469,-149.2 475,-143.2 481,-143.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="607" y="-583.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">mascotas (Pets & Veterinarians)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust3" class="cluster">
|
||||||
|
<title>cluster_productos</title>
|
||||||
|
<path fill="#e8f5e9" stroke="#2e7d32" d="M765,-496.4C765,-496.4 1141,-496.4 1141,-496.4 1147,-496.4 1153,-502.4 1153,-508.4 1153,-508.4 1153,-861.2 1153,-861.2 1153,-867.2 1147,-873.2 1141,-873.2 1141,-873.2 765,-873.2 765,-873.2 759,-873.2 753,-867.2 753,-861.2 753,-861.2 753,-508.4 753,-508.4 753,-502.4 759,-496.4 765,-496.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="953" y="-854" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">productos (Services & Pricing)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust4" class="cluster">
|
||||||
|
<title>cluster_solicitudes</title>
|
||||||
|
<path fill="#ffebee" stroke="#c62828" d="M1084,-143.2C1084,-143.2 1290,-143.2 1290,-143.2 1296,-143.2 1302,-149.2 1302,-155.2 1302,-155.2 1302,-455.6 1302,-455.6 1302,-461.6 1296,-467.6 1290,-467.6 1290,-467.6 1084,-467.6 1084,-467.6 1078,-467.6 1072,-461.6 1072,-455.6 1072,-455.6 1072,-155.2 1072,-155.2 1072,-149.2 1078,-143.2 1084,-143.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1187" y="-448.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">solicitudes (Service Requests)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust5" class="cluster">
|
||||||
|
<title>cluster_common</title>
|
||||||
|
<path fill="#f3e5f5" stroke="#7b1fa2" d="M20,-8C20,-8 774,-8 774,-8 780,-8 786,-14 786,-20 786,-20 786,-102.4 786,-102.4 786,-108.4 780,-114.4 774,-114.4 774,-114.4 20,-114.4 20,-114.4 14,-114.4 8,-108.4 8,-102.4 8,-102.4 8,-20 8,-20 8,-14 14,-8 20,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="397" y="-95.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">common (Shared Models)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust6" class="cluster">
|
||||||
|
<title>cluster_payments</title>
|
||||||
|
<path fill="#fce4ec" stroke="#ad1457" d="M1173,-496.4C1173,-496.4 1395,-496.4 1395,-496.4 1401,-496.4 1407,-502.4 1407,-508.4 1407,-508.4 1407,-590.8 1407,-590.8 1407,-596.8 1401,-602.8 1395,-602.8 1395,-602.8 1173,-602.8 1173,-602.8 1167,-602.8 1161,-596.8 1161,-590.8 1161,-590.8 1161,-508.4 1161,-508.4 1161,-502.4 1167,-496.4 1173,-496.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1284" y="-583.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">payments (Payment Processing)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust7" class="cluster">
|
||||||
|
<title>cluster_external</title>
|
||||||
|
<path fill="#fff3e0" stroke="#e65100" d="M1039,-8C1039,-8 1531,-8 1531,-8 1537,-8 1543,-14 1543,-20 1543,-20 1543,-102.4 1543,-102.4 1543,-108.4 1537,-114.4 1531,-114.4 1531,-114.4 1039,-114.4 1039,-114.4 1033,-114.4 1027,-108.4 1027,-102.4 1027,-102.4 1027,-20 1027,-20 1027,-14 1033,-8 1039,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1285" y="-95.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External Integrations</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust8" class="cluster">
|
||||||
|
<title>cluster_afip</title>
|
||||||
|
<path fill="#fffde7" stroke="#f9a825" d="M806,-8C806,-8 1007,-8 1007,-8 1013,-8 1019,-14 1019,-20 1019,-20 1019,-237.6 1019,-237.6 1019,-243.6 1013,-249.6 1007,-249.6 1007,-249.6 806,-249.6 806,-249.6 800,-249.6 794,-243.6 794,-237.6 794,-237.6 794,-20 794,-20 794,-14 800,-8 806,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="906.5" y="-230.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">django_afip (Invoicing)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>auth_user</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M440.93,-675.6C440.93,-675.6 381.07,-675.6 381.07,-675.6 375.07,-675.6 369.07,-669.6 369.07,-663.6 369.07,-663.6 369.07,-651.6 369.07,-651.6 369.07,-645.6 375.07,-639.6 381.07,-639.6 381.07,-639.6 440.93,-639.6 440.93,-639.6 446.93,-639.6 452.93,-645.6 452.93,-651.6 452.93,-651.6 452.93,-663.6 452.93,-663.6 452.93,-669.6 446.93,-675.6 440.93,-675.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="411" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">auth.User</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="411" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">(Django Auth)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>petowner</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M725.15,-540.4C725.15,-540.4 662.85,-540.4 662.85,-540.4 656.85,-540.4 650.85,-534.4 650.85,-528.4 650.85,-528.4 650.85,-516.4 650.85,-516.4 650.85,-510.4 656.85,-504.4 662.85,-504.4 662.85,-504.4 725.15,-504.4 725.15,-504.4 731.15,-504.4 737.15,-510.4 737.15,-516.4 737.15,-516.4 737.15,-528.4 737.15,-528.4 737.15,-534.4 731.15,-540.4 725.15,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="694" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">PetOwner</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="694" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Cliente/Tutor)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user->petowner -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>auth_user->petowner</title>
|
||||||
|
<path fill="none" stroke="#666666" d="M424.94,-639.31C424.94,-602.3 424.94,-522 424.94,-522 424.94,-522 638.96,-522 638.96,-522"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="638.96,-525.5 648.96,-522 638.96,-518.5 638.96,-525.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="645.27" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:1 optional</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>veterinarian</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M544.18,-405.2C544.18,-405.2 489.82,-405.2 489.82,-405.2 483.82,-405.2 477.82,-399.2 477.82,-393.2 477.82,-393.2 477.82,-381.2 477.82,-381.2 477.82,-375.2 483.82,-369.2 489.82,-369.2 489.82,-369.2 544.18,-369.2 544.18,-369.2 550.18,-369.2 556.18,-375.2 556.18,-381.2 556.18,-381.2 556.18,-393.2 556.18,-393.2 556.18,-399.2 550.18,-405.2 544.18,-405.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="517" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">Veterinarian</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="517" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Profesional)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user->veterinarian -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>auth_user->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#666666" d="M453.39,-664C477.9,-664 503.94,-664 503.94,-664 503.94,-664 503.94,-416.88 503.94,-416.88"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="507.44,-416.88 503.94,-406.88 500.44,-416.88 507.44,-416.88"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="470.26" y="-519.7" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- django_admin -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>django_admin</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M339.55,-675.6C339.55,-675.6 278.45,-675.6 278.45,-675.6 272.45,-675.6 266.45,-669.6 266.45,-663.6 266.45,-663.6 266.45,-651.6 266.45,-651.6 266.45,-645.6 272.45,-639.6 278.45,-639.6 278.45,-639.6 339.55,-639.6 339.55,-639.6 345.55,-639.6 351.55,-645.6 351.55,-651.6 351.55,-651.6 351.55,-663.6 351.55,-663.6 351.55,-669.6 345.55,-675.6 339.55,-675.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="309" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Django Admin</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="309" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">Interface</text>
|
||||||
|
</g>
|
||||||
|
<!-- drf -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>drf</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M236.63,-675.6C236.63,-675.6 177.37,-675.6 177.37,-675.6 171.37,-675.6 165.37,-669.6 165.37,-663.6 165.37,-663.6 165.37,-651.6 165.37,-651.6 165.37,-645.6 171.37,-639.6 177.37,-639.6 177.37,-639.6 236.63,-639.6 236.63,-639.6 242.63,-639.6 248.63,-645.6 248.63,-651.6 248.63,-651.6 248.63,-663.6 248.63,-663.6 248.63,-669.6 242.63,-675.6 236.63,-675.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="207" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Django REST</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="207" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">Framework</text>
|
||||||
|
</g>
|
||||||
|
<!-- jwt_auth -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>jwt_auth</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M264.69,-540.4C264.69,-540.4 177.31,-540.4 177.31,-540.4 171.31,-540.4 165.31,-534.4 165.31,-528.4 165.31,-528.4 165.31,-516.4 165.31,-516.4 165.31,-510.4 171.31,-504.4 177.31,-504.4 177.31,-504.4 264.69,-504.4 264.69,-504.4 270.69,-504.4 276.69,-510.4 276.69,-516.4 276.69,-516.4 276.69,-528.4 276.69,-528.4 276.69,-534.4 270.69,-540.4 264.69,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="221" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">JWT Authentication</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="221" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(SimpleJWT)</text>
|
||||||
|
</g>
|
||||||
|
<!-- drf->jwt_auth -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>drf->jwt_auth</title>
|
||||||
|
<path fill="none" stroke="#999999" stroke-dasharray="5,2" d="M207,-639.37C207,-639.37 207,-552.29 207,-552.29"/>
|
||||||
|
<polygon fill="#999999" stroke="#999999" points="210.5,-552.29 207,-542.29 203.5,-552.29 210.5,-552.29"/>
|
||||||
|
</g>
|
||||||
|
<!-- pet -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>pet</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M714.45,-405.2C714.45,-405.2 673.55,-405.2 673.55,-405.2 667.55,-405.2 661.55,-399.2 661.55,-393.2 661.55,-393.2 661.55,-381.2 661.55,-381.2 661.55,-375.2 667.55,-369.2 673.55,-369.2 673.55,-369.2 714.45,-369.2 714.45,-369.2 720.45,-369.2 726.45,-375.2 726.45,-381.2 726.45,-381.2 726.45,-393.2 726.45,-393.2 726.45,-399.2 720.45,-405.2 714.45,-405.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="694" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">Pet</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="694" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Mascota)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->pet -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>petowner->pet</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M694,-504.17C694,-504.17 694,-417.09 694,-417.09"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="697.5,-417.09 694,-407.09 690.5,-417.09 697.5,-417.09"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="701" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest -->
|
||||||
|
<g id="node18" class="node">
|
||||||
|
<title>servicerequest</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1163.82,-405.2C1163.82,-405.2 1094.18,-405.2 1094.18,-405.2 1088.18,-405.2 1082.18,-399.2 1082.18,-393.2 1082.18,-393.2 1082.18,-381.2 1082.18,-381.2 1082.18,-375.2 1088.18,-369.2 1094.18,-369.2 1094.18,-369.2 1163.82,-369.2 1163.82,-369.2 1169.82,-369.2 1175.82,-375.2 1175.82,-381.2 1175.82,-381.2 1175.82,-393.2 1175.82,-393.2 1175.82,-399.2 1169.82,-405.2 1163.82,-405.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1129" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">ServiceRequest</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1129" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Solicitud)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->servicerequest -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>petowner->servicerequest</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M729.12,-504.02C729.12,-468.96 729.12,-396 729.12,-396 729.12,-396 1070.44,-396 1070.44,-396"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="1070.44,-399.5 1080.44,-396 1070.44,-392.5 1070.44,-399.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1005" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- neighborhood -->
|
||||||
|
<g id="node27" class="node">
|
||||||
|
<title>neighborhood</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M109.63,-52C109.63,-52 28.37,-52 28.37,-52 22.37,-52 16.37,-46 16.37,-40 16.37,-40 16.37,-28 16.37,-28 16.37,-22 22.37,-16 28.37,-16 28.37,-16 109.63,-16 109.63,-16 115.63,-16 121.63,-22 121.63,-28 121.63,-28 121.63,-40 121.63,-40 121.63,-46 115.63,-52 109.63,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="69" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Neighborhood /</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="69" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">Province / Locality</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->neighborhood -->
|
||||||
|
<g id="edge24" class="edge">
|
||||||
|
<title>petowner->neighborhood</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M655.77,-504.11C655.77,-441.12 655.77,-237 655.77,-237 655.77,-237 86.54,-237 86.54,-237 86.54,-237 86.54,-63.89 86.54,-63.89"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="90.04,-63.89 86.54,-53.89 83.04,-63.89 90.04,-63.89"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="86" y="-301.7" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>vetvisit</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M725.37,-322.4C725.37,-322.4 682.63,-322.4 682.63,-322.4 676.63,-322.4 670.63,-316.4 670.63,-310.4 670.63,-310.4 670.63,-298.4 670.63,-298.4 670.63,-292.4 676.63,-286.4 682.63,-286.4 682.63,-286.4 725.37,-286.4 725.37,-286.4 731.37,-286.4 737.37,-292.4 737.37,-298.4 737.37,-298.4 737.37,-310.4 737.37,-310.4 737.37,-316.4 731.37,-322.4 725.37,-322.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="704" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">VetVisit</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="704" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Consulta)</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->vetvisit -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>pet->vetvisit</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M698.54,-368.82C698.54,-368.82 698.54,-334.24 698.54,-334.24"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="702.04,-334.24 698.54,-324.24 695.04,-334.24 702.04,-334.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="710.25" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet_health -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>pet_health</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M640.88,-322.4C640.88,-322.4 587.12,-322.4 587.12,-322.4 581.12,-322.4 575.12,-316.4 575.12,-310.4 575.12,-310.4 575.12,-298.4 575.12,-298.4 575.12,-292.4 581.12,-286.4 587.12,-286.4 587.12,-286.4 640.88,-286.4 640.88,-286.4 646.88,-286.4 652.88,-292.4 652.88,-298.4 652.88,-298.4 652.88,-310.4 652.88,-310.4 652.88,-316.4 646.88,-322.4 640.88,-322.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="614" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">PetVaccine /</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="614" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">PetStudy</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->pet_health -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>pet->pet_health</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M666.09,-368.72C666.09,-342.59 666.09,-298 666.09,-298 666.09,-298 664.73,-298 664.73,-298"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="664.73,-294.5 654.73,-298 664.73,-301.5 664.73,-294.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="623" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->vetvisit -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>veterinarian->vetvisit</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M556.46,-387C598.38,-387 658.66,-387 658.66,-387 658.66,-387 658.66,-310 658.66,-310 658.66,-310 659.85,-310 659.85,-310"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="659.06,-313.5 669.06,-310 659.06,-306.5 659.06,-313.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="668" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- availability -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>availability</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M545.4,-322.4C545.4,-322.4 488.6,-322.4 488.6,-322.4 482.6,-322.4 476.6,-316.4 476.6,-310.4 476.6,-310.4 476.6,-298.4 476.6,-298.4 476.6,-292.4 482.6,-286.4 488.6,-286.4 488.6,-286.4 545.4,-286.4 545.4,-286.4 551.4,-286.4 557.4,-292.4 557.4,-298.4 557.4,-298.4 557.4,-310.4 557.4,-310.4 557.4,-316.4 551.4,-322.4 545.4,-322.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="517" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">Availability /</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="517" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">Unavailability</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->availability -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>veterinarian->availability</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M503.94,-368.82C503.94,-368.82 503.94,-334.24 503.94,-334.24"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="507.44,-334.24 503.94,-324.24 500.44,-334.24 507.44,-334.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="524" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked -->
|
||||||
|
<g id="node20" class="node">
|
||||||
|
<title>vetasked</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1173.94,-322.4C1173.94,-322.4 1092.06,-322.4 1092.06,-322.4 1086.06,-322.4 1080.06,-316.4 1080.06,-310.4 1080.06,-310.4 1080.06,-298.4 1080.06,-298.4 1080.06,-292.4 1086.06,-286.4 1092.06,-286.4 1092.06,-286.4 1173.94,-286.4 1173.94,-286.4 1179.94,-286.4 1185.94,-292.4 1185.94,-298.4 1185.94,-298.4 1185.94,-310.4 1185.94,-310.4 1185.94,-316.4 1179.94,-322.4 1173.94,-322.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1133" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">VeterinarianAsked</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1133" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Vet Consultado)</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->vetasked -->
|
||||||
|
<g id="edge23" class="edge">
|
||||||
|
<title>veterinarian->vetasked</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M530.06,-368.88C530.06,-354.89 530.06,-338 530.06,-338 530.06,-338 1129,-338 1129,-338 1129,-338 1129,-334.29 1129,-334.29"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1132.5,-334.29 1129,-324.29 1125.5,-334.29 1132.5,-334.29"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="959" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- specialty -->
|
||||||
|
<g id="node24" class="node">
|
||||||
|
<title>specialty</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M224.66,-52C224.66,-52 151.34,-52 151.34,-52 145.34,-52 139.34,-46 139.34,-40 139.34,-40 139.34,-28 139.34,-28 139.34,-22 145.34,-16 151.34,-16 151.34,-16 224.66,-16 224.66,-16 230.66,-16 236.66,-22 236.66,-28 236.66,-28 236.66,-40 236.66,-40 236.66,-46 230.66,-52 224.66,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="188" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Specialty</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="188" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Especialidades)</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->specialty -->
|
||||||
|
<g id="edge25" class="edge">
|
||||||
|
<title>veterinarian->specialty</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M477.62,-387C392.9,-387 200.99,-387 200.99,-387 200.99,-387 200.99,-64 200.99,-64"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="204.49,-64 200.99,-54 197.49,-64 204.49,-64"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="289.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->neighborhood -->
|
||||||
|
<g id="edge26" class="edge">
|
||||||
|
<title>veterinarian->neighborhood</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M477.71,-396C365.47,-396 51.46,-396 51.46,-396 51.46,-396 51.46,-63.96 51.46,-63.96"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="54.96,-63.96 51.46,-53.96 47.96,-63.96 54.96,-63.96"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="158.01" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M coverage</text>
|
||||||
|
</g>
|
||||||
|
<!-- turnfee -->
|
||||||
|
<g id="node28" class="node">
|
||||||
|
<title>turnfee</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M347.02,-52C347.02,-52 266.98,-52 266.98,-52 260.98,-52 254.98,-46 254.98,-40 254.98,-40 254.98,-28 254.98,-28 254.98,-22 260.98,-16 266.98,-16 266.98,-16 347.02,-16 347.02,-16 353.02,-16 359.02,-22 359.02,-28 359.02,-28 359.02,-40 359.02,-40 359.02,-46 353.02,-52 347.02,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="307" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">IndividualTurnFee</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="307" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">Group</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->turnfee -->
|
||||||
|
<g id="edge30" class="edge">
|
||||||
|
<title>veterinarian->turnfee</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M477.44,-378C429.56,-378 355.29,-378 355.29,-378 355.29,-378 355.29,-64.01 355.29,-64.01"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="358.79,-64.01 355.29,-54.01 351.79,-64.01 358.79,-64.01"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="364.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>vetvisitreport</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M694.34,-187.2C694.34,-187.2 621.66,-187.2 621.66,-187.2 615.66,-187.2 609.66,-181.2 609.66,-175.2 609.66,-175.2 609.66,-163.2 609.66,-163.2 609.66,-157.2 615.66,-151.2 621.66,-151.2 621.66,-151.2 694.34,-151.2 694.34,-151.2 700.34,-151.2 706.34,-157.2 706.34,-163.2 706.34,-163.2 706.34,-175.2 706.34,-175.2 706.34,-181.2 700.34,-187.2 694.34,-187.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="658" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">VetVisitReport</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="658" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Informe Clinico)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->vetvisitreport -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>vetvisit->vetvisitreport</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M688.49,-286.17C688.49,-286.17 688.49,-199.09 688.49,-199.09"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="691.99,-199.09 688.49,-189.09 684.99,-199.09 691.99,-199.09"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="671" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->servicerequest -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>vetvisit->servicerequest</title>
|
||||||
|
<path fill="none" stroke="#1565c0" stroke-dasharray="5,2" d="M731.8,-322.73C731.8,-347.07 731.8,-387 731.8,-387 731.8,-387 1070.36,-387 1070.36,-387"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="1070.36,-390.5 1080.36,-387 1070.36,-383.5 1070.36,-390.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="749.26" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- google_cal -->
|
||||||
|
<g id="node31" class="node">
|
||||||
|
<title>google_cal</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M1122.58,-52C1122.58,-52 1047.42,-52 1047.42,-52 1041.42,-52 1035.42,-46 1035.42,-40 1035.42,-40 1035.42,-28 1035.42,-28 1035.42,-22 1041.42,-16 1047.42,-16 1047.42,-16 1122.58,-16 1122.58,-16 1128.58,-16 1134.58,-22 1134.58,-28 1134.58,-28 1134.58,-40 1134.58,-40 1134.58,-46 1128.58,-52 1122.58,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1085" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Google Calendar</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1085" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Agenda)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->google_cal -->
|
||||||
|
<g id="edge33" class="edge">
|
||||||
|
<title>vetvisit->google_cal</title>
|
||||||
|
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M737.59,-304C824.11,-304 1047.82,-304 1047.82,-304 1047.82,-304 1047.82,-63.65 1047.82,-63.65"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1051.32,-63.65 1047.82,-53.65 1044.32,-63.65 1051.32,-63.65"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1054.25" y="-166.5" font-family="Helvetica,sans-Serif" font-size="9.00">sync</text>
|
||||||
|
</g>
|
||||||
|
<!-- receipt -->
|
||||||
|
<g id="node36" class="node">
|
||||||
|
<title>receipt</title>
|
||||||
|
<path fill="#fff9c4" stroke="black" d="M880.99,-187.2C880.99,-187.2 815.01,-187.2 815.01,-187.2 809.01,-187.2 803.01,-181.2 803.01,-175.2 803.01,-175.2 803.01,-163.2 803.01,-163.2 803.01,-157.2 809.01,-151.2 815.01,-151.2 815.01,-151.2 880.99,-151.2 880.99,-151.2 886.99,-151.2 892.99,-157.2 892.99,-163.2 892.99,-163.2 892.99,-175.2 892.99,-175.2 892.99,-181.2 886.99,-187.2 880.99,-187.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="848" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">Receipt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="848" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Comprobante)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->receipt -->
|
||||||
|
<g id="edge36" class="edge">
|
||||||
|
<title>vetvisit->receipt</title>
|
||||||
|
<path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M722.89,-286.14C722.89,-249.18 722.89,-169 722.89,-169 722.89,-169 791.32,-169 791.32,-169"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="791.32,-172.5 801.32,-169 791.32,-165.5 791.32,-172.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="727.27" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:1 optional</text>
|
||||||
|
</g>
|
||||||
|
<!-- medication -->
|
||||||
|
<g id="node25" class="node">
|
||||||
|
<title>medication</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M678.43,-52C678.43,-52 607.57,-52 607.57,-52 601.57,-52 595.57,-46 595.57,-40 595.57,-40 595.57,-28 595.57,-28 595.57,-22 601.57,-16 607.57,-16 607.57,-16 678.43,-16 678.43,-16 684.43,-16 690.43,-22 690.43,-28 690.43,-28 690.43,-40 690.43,-40 690.43,-46 684.43,-52 678.43,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="643" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Medication</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="643" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Medicamentos)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport->medication -->
|
||||||
|
<g id="edge29" class="edge">
|
||||||
|
<title>vetvisitreport->medication</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M650.04,-150.97C650.04,-150.97 650.04,-63.89 650.04,-63.89"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="653.54,-63.89 650.04,-53.89 646.54,-63.89 653.54,-63.89"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="667.26" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">references</text>
|
||||||
|
</g>
|
||||||
|
<!-- service -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>service</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M972.22,-675.6C972.22,-675.6 933.78,-675.6 933.78,-675.6 927.78,-675.6 921.78,-669.6 921.78,-663.6 921.78,-663.6 921.78,-651.6 921.78,-651.6 921.78,-645.6 927.78,-639.6 933.78,-639.6 933.78,-639.6 972.22,-639.6 972.22,-639.6 978.22,-639.6 984.22,-645.6 984.22,-651.6 984.22,-651.6 984.22,-663.6 984.22,-663.6 984.22,-669.6 978.22,-675.6 972.22,-675.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="953" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Service</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="953" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">(Servicio)</text>
|
||||||
|
</g>
|
||||||
|
<!-- category -->
|
||||||
|
<g id="node13" class="node">
|
||||||
|
<title>category</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M849.19,-540.4C849.19,-540.4 772.81,-540.4 772.81,-540.4 766.81,-540.4 760.81,-534.4 760.81,-528.4 760.81,-528.4 760.81,-516.4 760.81,-516.4 760.81,-510.4 766.81,-504.4 772.81,-504.4 772.81,-504.4 849.19,-504.4 849.19,-504.4 855.19,-504.4 861.19,-510.4 861.19,-516.4 861.19,-516.4 861.19,-528.4 861.19,-528.4 861.19,-534.4 855.19,-540.4 849.19,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="811" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Category / Group</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="811" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Categorias)</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->category -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>service->category</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M921.49,-652C879.65,-652 811,-652 811,-652 811,-652 811,-552.21 811,-552.21"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="814.5,-552.21 811,-542.21 807.5,-552.21 814.5,-552.21"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="835" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- prices -->
|
||||||
|
<g id="node14" class="node">
|
||||||
|
<title>prices</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M927,-540.4C927,-540.4 891,-540.4 891,-540.4 885,-540.4 879,-534.4 879,-528.4 879,-528.4 879,-516.4 879,-516.4 879,-510.4 885,-504.4 891,-504.4 891,-504.4 927,-504.4 927,-504.4 933,-504.4 939,-510.4 939,-516.4 939,-516.4 939,-528.4 939,-528.4 939,-534.4 933,-540.4 927,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="909" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Prices</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="909" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Precios)</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->prices -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>service->prices</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M930.39,-639.37C930.39,-639.37 930.39,-552.29 930.39,-552.29"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="933.89,-552.29 930.39,-542.29 926.89,-552.29 933.89,-552.29"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="929" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- discounts -->
|
||||||
|
<g id="node15" class="node">
|
||||||
|
<title>discounts</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M1026.71,-540.4C1026.71,-540.4 969.29,-540.4 969.29,-540.4 963.29,-540.4 957.29,-534.4 957.29,-528.4 957.29,-528.4 957.29,-516.4 957.29,-516.4 957.29,-510.4 963.29,-504.4 969.29,-504.4 969.29,-504.4 1026.71,-504.4 1026.71,-504.4 1032.71,-504.4 1038.71,-510.4 1038.71,-516.4 1038.71,-516.4 1038.71,-528.4 1038.71,-528.4 1038.71,-534.4 1032.71,-540.4 1026.71,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="998" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Discounts</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="998" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Descuentos)</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->discounts -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>service->discounts</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M970.76,-639.37C970.76,-639.37 970.76,-552.29 970.76,-552.29"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="974.26,-552.29 970.76,-542.29 967.26,-552.29 974.26,-552.29"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="992" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart -->
|
||||||
|
<g id="node16" class="node">
|
||||||
|
<title>cart</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M1133.06,-540.4C1133.06,-540.4 1068.94,-540.4 1068.94,-540.4 1062.94,-540.4 1056.94,-534.4 1056.94,-528.4 1056.94,-528.4 1056.94,-516.4 1056.94,-516.4 1056.94,-510.4 1062.94,-504.4 1068.94,-504.4 1068.94,-504.4 1133.06,-504.4 1133.06,-504.4 1139.06,-504.4 1145.06,-510.4 1145.06,-516.4 1145.06,-516.4 1145.06,-528.4 1145.06,-528.4 1145.06,-534.4 1139.06,-540.4 1133.06,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1101" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Cart / CartItem</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1101" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Carrito)</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->cart -->
|
||||||
|
<g id="edge17" class="edge">
|
||||||
|
<title>service->cart</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M984.39,-658C1027.83,-658 1101,-658 1101,-658 1101,-658 1101,-552.2 1101,-552.2"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1104.5,-552.2 1101,-542.2 1097.5,-552.2 1104.5,-552.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1089.26" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">via CartItem</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->petowner -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>cart->petowner</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M1064.64,-504.13C1064.64,-489.36 1064.64,-471 1064.64,-471 1064.64,-471 734.47,-471 734.47,-471 734.47,-471 734.47,-492.62 734.47,-492.62"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="730.97,-492.62 734.47,-502.62 737.97,-492.62 730.97,-492.62"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="871" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->veterinarian -->
|
||||||
|
<g id="edge16" class="edge">
|
||||||
|
<title>cart->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M1072.35,-504.09C1072.35,-479.26 1072.35,-438 1072.35,-438 1072.35,-438 530.06,-438 530.06,-438 530.06,-438 530.06,-417.05 530.06,-417.05"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="533.56,-417.05 530.06,-407.05 526.56,-417.05 533.56,-417.05"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="886.02" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:1 optional</text>
|
||||||
|
</g>
|
||||||
|
<!-- combo -->
|
||||||
|
<g id="node17" class="node">
|
||||||
|
<title>combo</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M985.07,-810.8C985.07,-810.8 920.93,-810.8 920.93,-810.8 914.93,-810.8 908.93,-804.8 908.93,-798.8 908.93,-798.8 908.93,-786.8 908.93,-786.8 908.93,-780.8 914.93,-774.8 920.93,-774.8 920.93,-774.8 985.07,-774.8 985.07,-774.8 991.07,-774.8 997.07,-780.8 997.07,-786.8 997.07,-786.8 997.07,-798.8 997.07,-798.8 997.07,-804.8 991.07,-810.8 985.07,-810.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="953" y="-796.1" font-family="Helvetica,sans-Serif" font-size="11.00">ServiceCombo</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="953" y="-782.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Paquetes)</text>
|
||||||
|
</g>
|
||||||
|
<!-- combo->service -->
|
||||||
|
<g id="edge18" class="edge">
|
||||||
|
<title>combo->service</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M953,-774.57C953,-774.57 953,-687.49 953,-687.49"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="956.5,-687.49 953,-677.49 949.5,-687.49 956.5,-687.49"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="969.76" y="-748.7" font-family="Helvetica,sans-Serif" font-size="9.00">contains</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->cart -->
|
||||||
|
<g id="edge19" class="edge">
|
||||||
|
<title>servicerequest->cart</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1113.62,-405.43C1113.62,-405.43 1113.62,-492.51 1113.62,-492.51"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1110.12,-492.51 1113.62,-502.51 1117.12,-492.51 1110.12,-492.51"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1121.26" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- statehistory -->
|
||||||
|
<g id="node19" class="node">
|
||||||
|
<title>statehistory</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1267.95,-322.4C1267.95,-322.4 1216.05,-322.4 1216.05,-322.4 1210.05,-322.4 1204.05,-316.4 1204.05,-310.4 1204.05,-310.4 1204.05,-298.4 1204.05,-298.4 1204.05,-292.4 1210.05,-286.4 1216.05,-286.4 1216.05,-286.4 1267.95,-286.4 1267.95,-286.4 1273.95,-286.4 1279.95,-292.4 1279.95,-298.4 1279.95,-298.4 1279.95,-310.4 1279.95,-310.4 1279.95,-316.4 1273.95,-322.4 1267.95,-322.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1242" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">StateHistory</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1242" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Historial)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->statehistory -->
|
||||||
|
<g id="edge20" class="edge">
|
||||||
|
<title>servicerequest->statehistory</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1176.2,-378C1201.95,-378 1228.64,-378 1228.64,-378 1228.64,-378 1228.64,-334.11 1228.64,-334.11"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1232.14,-334.11 1228.64,-324.11 1225.14,-334.11 1232.14,-334.11"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1212" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->vetasked -->
|
||||||
|
<g id="edge21" class="edge">
|
||||||
|
<title>servicerequest->vetasked</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1152.41,-368.82C1152.41,-368.82 1152.41,-334.24 1152.41,-334.24"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1155.91,-334.24 1152.41,-324.24 1148.91,-334.24 1155.91,-334.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1137" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- campaign -->
|
||||||
|
<g id="node22" class="node">
|
||||||
|
<title>campaign</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M436.81,-52C436.81,-52 389.19,-52 389.19,-52 383.19,-52 377.19,-46 377.19,-40 377.19,-40 377.19,-28 377.19,-28 377.19,-22 383.19,-16 389.19,-16 389.19,-16 436.81,-16 436.81,-16 442.81,-16 448.81,-22 448.81,-28 448.81,-28 448.81,-40 448.81,-40 448.81,-46 442.81,-52 436.81,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="413" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Campaign</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="413" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Marketing)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->campaign -->
|
||||||
|
<g id="edge27" class="edge">
|
||||||
|
<title>servicerequest->campaign</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M1105.59,-369.05C1105.59,-361.29 1105.59,-354 1105.59,-354 1105.59,-354 401.06,-354 401.06,-354 401.06,-354 401.06,-63.63 401.06,-63.63"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="404.56,-63.63 401.06,-53.63 397.56,-63.64 404.56,-63.63"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="437.02" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:1 optional</text>
|
||||||
|
</g>
|
||||||
|
<!-- tag -->
|
||||||
|
<g id="node23" class="node">
|
||||||
|
<title>tag</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M765.59,-52C765.59,-52 720.41,-52 720.41,-52 714.41,-52 708.41,-46 708.41,-40 708.41,-40 708.41,-28 708.41,-28 708.41,-22 714.41,-16 720.41,-16 720.41,-16 765.59,-16 765.59,-16 771.59,-16 777.59,-22 777.59,-28 777.59,-28 777.59,-40 777.59,-40 777.59,-46 771.59,-52 765.59,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="743" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Tag</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="743" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Etiquetas)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->tag -->
|
||||||
|
<g id="edge28" class="edge">
|
||||||
|
<title>servicerequest->tag</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M1081.96,-378C984.44,-378 769.2,-378 769.2,-378 769.2,-378 769.2,-64.01 769.2,-64.01"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="772.7,-64.01 769.2,-54.01 765.7,-64.01 772.7,-64.01"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="783.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
|
||||||
|
</g>
|
||||||
|
<!-- mercadopago -->
|
||||||
|
<g id="node29" class="node">
|
||||||
|
<title>mercadopago</title>
|
||||||
|
<path fill="#f8bbd9" stroke="black" d="M1241.24,-540.4C1241.24,-540.4 1180.76,-540.4 1180.76,-540.4 1174.76,-540.4 1168.76,-534.4 1168.76,-528.4 1168.76,-528.4 1168.76,-516.4 1168.76,-516.4 1168.76,-510.4 1174.76,-504.4 1180.76,-504.4 1180.76,-504.4 1241.24,-504.4 1241.24,-504.4 1247.24,-504.4 1253.24,-510.4 1253.24,-516.4 1253.24,-516.4 1253.24,-528.4 1253.24,-528.4 1253.24,-534.4 1247.24,-540.4 1241.24,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1211" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">MercadoPago</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1211" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">Account</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->mercadopago -->
|
||||||
|
<g id="edge31" class="edge">
|
||||||
|
<title>servicerequest->mercadopago</title>
|
||||||
|
<path fill="none" stroke="#ad1457" d="M1172.29,-405.43C1172.29,-405.43 1172.29,-492.51 1172.29,-492.51"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="1168.79,-492.51 1172.29,-502.51 1175.79,-492.51 1168.79,-492.51"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1205.26" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">payment</text>
|
||||||
|
</g>
|
||||||
|
<!-- mercately -->
|
||||||
|
<g id="node33" class="node">
|
||||||
|
<title>mercately</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M1523.04,-52C1523.04,-52 1472.96,-52 1472.96,-52 1466.96,-52 1460.96,-46 1460.96,-40 1460.96,-40 1460.96,-28 1460.96,-28 1460.96,-22 1466.96,-16 1472.96,-16 1472.96,-16 1523.04,-16 1523.04,-16 1529.04,-16 1535.04,-22 1535.04,-28 1535.04,-28 1535.04,-40 1535.04,-40 1535.04,-46 1529.04,-52 1523.04,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1498" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Mercately</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1498" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(WhatsApp)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->mercately -->
|
||||||
|
<g id="edge34" class="edge">
|
||||||
|
<title>servicerequest->mercately</title>
|
||||||
|
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1176.11,-387C1275.59,-387 1498,-387 1498,-387 1498,-387 1498,-64 1498,-64"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1501.5,-64 1498,-54 1494.5,-64 1501.5,-64"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1350.76" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">notify</text>
|
||||||
|
</g>
|
||||||
|
<!-- reminders -->
|
||||||
|
<g id="node21" class="node">
|
||||||
|
<title>reminders</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1249.9,-187.2C1249.9,-187.2 1182.1,-187.2 1182.1,-187.2 1176.1,-187.2 1170.1,-181.2 1170.1,-175.2 1170.1,-175.2 1170.1,-163.2 1170.1,-163.2 1170.1,-157.2 1176.1,-151.2 1182.1,-151.2 1182.1,-151.2 1249.9,-151.2 1249.9,-151.2 1255.9,-151.2 1261.9,-157.2 1261.9,-163.2 1261.9,-163.2 1261.9,-175.2 1261.9,-175.2 1261.9,-181.2 1255.9,-187.2 1249.9,-187.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1216" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">Reminders</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1216" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Recordatorios)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked->reminders -->
|
||||||
|
<g id="edge22" class="edge">
|
||||||
|
<title>vetasked->reminders</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1178.02,-286.17C1178.02,-286.17 1178.02,-199.09 1178.02,-199.09"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1181.52,-199.09 1178.02,-189.09 1174.52,-199.09 1181.52,-199.09"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1181" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- celery -->
|
||||||
|
<g id="node35" class="node">
|
||||||
|
<title>celery</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M1332.84,-52C1332.84,-52 1271.16,-52 1271.16,-52 1265.16,-52 1259.16,-46 1259.16,-40 1259.16,-40 1259.16,-28 1259.16,-28 1259.16,-22 1265.16,-16 1271.16,-16 1271.16,-16 1332.84,-16 1332.84,-16 1338.84,-16 1344.84,-22 1344.84,-28 1344.84,-28 1344.84,-40 1344.84,-40 1344.84,-46 1338.84,-52 1332.84,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1302" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Celery</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1302" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Async Tasks)</text>
|
||||||
|
</g>
|
||||||
|
<!-- reminders->celery -->
|
||||||
|
<g id="edge35" class="edge">
|
||||||
|
<title>reminders->celery</title>
|
||||||
|
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1250.27,-150.97C1250.27,-114.06 1250.27,-34 1250.27,-34 1250.27,-34 1251.13,-34 1251.13,-34"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1247.3,-37.5 1257.3,-34 1247.3,-30.5 1247.3,-37.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1270.52" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">scheduled</text>
|
||||||
|
</g>
|
||||||
|
<!-- breed -->
|
||||||
|
<g id="node26" class="node">
|
||||||
|
<title>breed</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M565.08,-52C565.08,-52 478.92,-52 478.92,-52 472.92,-52 466.92,-46 466.92,-40 466.92,-40 466.92,-28 466.92,-28 466.92,-22 472.92,-16 478.92,-16 478.92,-16 565.08,-16 565.08,-16 571.08,-16 577.08,-22 577.08,-28 577.08,-28 577.08,-40 577.08,-40 577.08,-46 571.08,-52 565.08,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="522" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">PetBreed / Vaccine</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="522" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/ Study</text>
|
||||||
|
</g>
|
||||||
|
<!-- mpnotification -->
|
||||||
|
<g id="node30" class="node">
|
||||||
|
<title>mpnotification</title>
|
||||||
|
<path fill="#f8bbd9" stroke="black" d="M1348.99,-540.4C1348.99,-540.4 1283.01,-540.4 1283.01,-540.4 1277.01,-540.4 1271.01,-534.4 1271.01,-528.4 1271.01,-528.4 1271.01,-516.4 1271.01,-516.4 1271.01,-510.4 1277.01,-504.4 1283.01,-504.4 1283.01,-504.4 1348.99,-504.4 1348.99,-504.4 1354.99,-504.4 1360.99,-510.4 1360.99,-516.4 1360.99,-516.4 1360.99,-528.4 1360.99,-528.4 1360.99,-534.4 1354.99,-540.4 1348.99,-540.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1316" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">MP Notification</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1316" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Webhooks)</text>
|
||||||
|
</g>
|
||||||
|
<!-- mpnotification->servicerequest -->
|
||||||
|
<g id="edge32" class="edge">
|
||||||
|
<title>mpnotification->servicerequest</title>
|
||||||
|
<path fill="none" stroke="#ad1457" d="M1275.48,-504.02C1275.48,-468.96 1275.48,-396 1275.48,-396 1275.48,-396 1187.67,-396 1187.67,-396"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="1187.67,-392.5 1177.67,-396 1187.67,-399.5 1187.67,-392.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1305" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">confirms</text>
|
||||||
|
</g>
|
||||||
|
<!-- google_sheets -->
|
||||||
|
<g id="node32" class="node">
|
||||||
|
<title>google_sheets</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M1229.39,-52C1229.39,-52 1164.61,-52 1164.61,-52 1158.61,-52 1152.61,-46 1152.61,-40 1152.61,-40 1152.61,-28 1152.61,-28 1152.61,-22 1158.61,-16 1164.61,-16 1164.61,-16 1229.39,-16 1229.39,-16 1235.39,-16 1241.39,-22 1241.39,-28 1241.39,-28 1241.39,-40 1241.39,-40 1241.39,-46 1235.39,-52 1229.39,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1197" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Google Sheets</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1197" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Exports)</text>
|
||||||
|
</g>
|
||||||
|
<!-- afip -->
|
||||||
|
<g id="node34" class="node">
|
||||||
|
<title>afip</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M1431.4,-52C1431.4,-52 1374.6,-52 1374.6,-52 1368.6,-52 1362.6,-46 1362.6,-40 1362.6,-40 1362.6,-28 1362.6,-28 1362.6,-22 1368.6,-16 1374.6,-16 1374.6,-16 1431.4,-16 1431.4,-16 1437.4,-16 1443.4,-22 1443.4,-28 1443.4,-28 1443.4,-40 1443.4,-40 1443.4,-46 1437.4,-52 1431.4,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1403" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">AFIP</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1403" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Facturacion)</text>
|
||||||
|
</g>
|
||||||
|
<!-- taxpayer -->
|
||||||
|
<g id="node37" class="node">
|
||||||
|
<title>taxpayer</title>
|
||||||
|
<path fill="#fff9c4" stroke="black" d="M881.91,-52C881.91,-52 814.09,-52 814.09,-52 808.09,-52 802.09,-46 802.09,-40 802.09,-40 802.09,-28 802.09,-28 802.09,-22 808.09,-16 814.09,-16 814.09,-16 881.91,-16 881.91,-16 887.91,-16 893.91,-22 893.91,-28 893.91,-28 893.91,-40 893.91,-40 893.91,-46 887.91,-52 881.91,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="848" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">TaxPayer</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="848" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Contribuyente)</text>
|
||||||
|
</g>
|
||||||
|
<!-- receipt->taxpayer -->
|
||||||
|
<g id="edge37" class="edge">
|
||||||
|
<title>receipt->taxpayer</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M848,-150.97C848,-150.97 848,-63.89 848,-63.89"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="851.5,-63.89 848,-53.89 844.5,-63.89 851.5,-63.89"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="855" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- pos -->
|
||||||
|
<g id="node38" class="node">
|
||||||
|
<title>pos</title>
|
||||||
|
<path fill="#fff9c4" stroke="black" d="M998.58,-52C998.58,-52 923.42,-52 923.42,-52 917.42,-52 911.42,-46 911.42,-40 911.42,-40 911.42,-28 911.42,-28 911.42,-22 917.42,-16 923.42,-16 923.42,-16 998.58,-16 998.58,-16 1004.58,-16 1010.58,-22 1010.58,-28 1010.58,-28 1010.58,-40 1010.58,-40 1010.58,-46 1004.58,-52 998.58,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="961" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">PointOfSales</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="961" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Punto de Venta)</text>
|
||||||
|
</g>
|
||||||
|
<!-- receipt->pos -->
|
||||||
|
<g id="edge38" class="edge">
|
||||||
|
<title>receipt->pos</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M893.36,-169C910.17,-169 925.21,-169 925.21,-169 925.21,-169 925.21,-64 925.21,-64"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="928.71,-64 925.21,-54 921.71,-64 928.71,-64"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="930" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 50 KiB |
184
cfg/amar/soleprint/atlas/books/arch/02-frontend-architecture.dot
Normal file
184
cfg/amar/soleprint/atlas/books/arch/02-frontend-architecture.dot
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
digraph FrontendArchitecture {
|
||||||
|
// Graph settings
|
||||||
|
rankdir=TB
|
||||||
|
compound=true
|
||||||
|
splines=ortho
|
||||||
|
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11]
|
||||||
|
edge [fontname="Helvetica", fontsize=9]
|
||||||
|
|
||||||
|
label="AMAR Mascotas - Frontend Architecture (Next.js)\n\n"
|
||||||
|
labelloc="t"
|
||||||
|
fontsize=16
|
||||||
|
fontname="Helvetica-Bold"
|
||||||
|
|
||||||
|
// Next.js Core Cluster
|
||||||
|
subgraph cluster_nextjs {
|
||||||
|
label="Next.js 13+ (App Router)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E3F2FD"
|
||||||
|
color="#1565C0"
|
||||||
|
|
||||||
|
app_router [label="App Router\n/app/*", fillcolor="#BBDEFB"]
|
||||||
|
layout [label="Layout Components\n(RootLayout, etc)", fillcolor="#BBDEFB"]
|
||||||
|
middleware [label="Middleware\n(Auth redirect)", fillcolor="#BBDEFB"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Frontend Cluster
|
||||||
|
subgraph cluster_public {
|
||||||
|
label="Public Frontend (/(frontend))"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E8F5E9"
|
||||||
|
color="#2E7D32"
|
||||||
|
|
||||||
|
home [label="Home Page\n/", fillcolor="#C8E6C9"]
|
||||||
|
services_page [label="Services Catalog\n/servicios", fillcolor="#C8E6C9"]
|
||||||
|
cart_page [label="Cart\n/carrito", fillcolor="#C8E6C9"]
|
||||||
|
login_page [label="Login/Register\n/login", fillcolor="#C8E6C9"]
|
||||||
|
profile_page [label="User Profile\n/perfil", fillcolor="#C8E6C9"]
|
||||||
|
pets_page [label="My Pets\n/mascotas", fillcolor="#C8E6C9"]
|
||||||
|
requests_page [label="My Requests\n/solicitudes", fillcolor="#C8E6C9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoffice Cluster
|
||||||
|
subgraph cluster_backoffice {
|
||||||
|
label="Backoffice (/(backoffice)/admin)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFF3E0"
|
||||||
|
color="#E65100"
|
||||||
|
|
||||||
|
admin_dash [label="Dashboard\n/admin", fillcolor="#FFE0B2"]
|
||||||
|
admin_visits [label="Visits Management\n/admin/visits", fillcolor="#FFE0B2"]
|
||||||
|
admin_pets [label="Pets Overview\n/admin/pets", fillcolor="#FFE0B2"]
|
||||||
|
admin_requests [label="Service Requests\n/admin/solicitudes", fillcolor="#FFE0B2"]
|
||||||
|
admin_calendar [label="Calendar View\n/admin/calendario", fillcolor="#FFE0B2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Components Cluster
|
||||||
|
subgraph cluster_components {
|
||||||
|
label="Shared Components (/components)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#F3E5F5"
|
||||||
|
color="#7B1FA2"
|
||||||
|
|
||||||
|
sidebar [label="Sidebar\nNavigation", fillcolor="#E1BEE7"]
|
||||||
|
navbar [label="NavbarBackoffice\nTop Bar", fillcolor="#E1BEE7"]
|
||||||
|
visits_section [label="VisitsSection\n(List + Actions)", fillcolor="#E1BEE7"]
|
||||||
|
drawer [label="VisitsDrawer\n(Side Panel)", fillcolor="#E1BEE7"]
|
||||||
|
tables [label="DataTable\nComponents", fillcolor="#E1BEE7"]
|
||||||
|
forms [label="Form Components\n(Pet, Visit, etc)", fillcolor="#E1BEE7"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services Layer Cluster
|
||||||
|
subgraph cluster_services {
|
||||||
|
label="Services Layer (/services)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFEBEE"
|
||||||
|
color="#C62828"
|
||||||
|
|
||||||
|
http_service [label="HttpService\n(Axios wrapper)", fillcolor="#FFCDD2"]
|
||||||
|
auth_api [label="authAPI\n(login/register)", fillcolor="#FFCDD2"]
|
||||||
|
visits_api [label="visitsAPI\n(CRUD visits)", fillcolor="#FFCDD2"]
|
||||||
|
orders_api [label="OrdersAPI\n(service requests)", fillcolor="#FFCDD2"]
|
||||||
|
petowners_api [label="petOwnersAPI\n(clients)", fillcolor="#FFCDD2"]
|
||||||
|
vets_api [label="VeterinariansAPI\n(professionals)", fillcolor="#FFCDD2"]
|
||||||
|
services_api [label="servicesAPI\n(catalog)", fillcolor="#FFCDD2"]
|
||||||
|
cart_api [label="CartAPI\n(shopping)", fillcolor="#FFCDD2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// State Management Cluster
|
||||||
|
subgraph cluster_state {
|
||||||
|
label="State Management (/redux, /contexts)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E0F7FA"
|
||||||
|
color="#00838F"
|
||||||
|
|
||||||
|
redux_store [label="Redux Store\n(Global State)", fillcolor="#B2EBF2"]
|
||||||
|
auth_slice [label="Auth Slice\n(user, token)", fillcolor="#B2EBF2"]
|
||||||
|
visits_slice [label="Visits Slice\n(visit data)", fillcolor="#B2EBF2"]
|
||||||
|
cart_slice [label="Cart Slice\n(items)", fillcolor="#B2EBF2"]
|
||||||
|
auth_context [label="AuthContext\n(Provider)", fillcolor="#B2EBF2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend API Cluster (external)
|
||||||
|
subgraph cluster_backend {
|
||||||
|
label="Django Backend API"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#ECEFF1"
|
||||||
|
color="#455A64"
|
||||||
|
|
||||||
|
api_mascotas [label="/mascotas/api/v1/\n(Pets, Vets, Visits)", fillcolor="#CFD8DC"]
|
||||||
|
api_productos [label="/productos/\n(Services, Prices)", fillcolor="#CFD8DC"]
|
||||||
|
api_solicitudes [label="/solicitudes/\n(Requests)", fillcolor="#CFD8DC"]
|
||||||
|
api_auth [label="/api/token/\n(JWT Auth)", fillcolor="#CFD8DC"]
|
||||||
|
api_payments [label="/payments/\n(MercadoPago)", fillcolor="#CFD8DC"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Types
|
||||||
|
subgraph cluster_users {
|
||||||
|
label="User Types"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FCE4EC"
|
||||||
|
color="#AD1457"
|
||||||
|
|
||||||
|
petowner_user [label="PetOwner\n(Cliente)", shape=ellipse, fillcolor="#F8BBD9"]
|
||||||
|
vet_user [label="Veterinarian\n(Profesional)", shape=ellipse, fillcolor="#F8BBD9"]
|
||||||
|
admin_user [label="Admin/Staff\n(Interno)", shape=ellipse, fillcolor="#F8BBD9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relationships - Router
|
||||||
|
app_router -> layout [color="#1565C0"]
|
||||||
|
app_router -> middleware [color="#1565C0"]
|
||||||
|
|
||||||
|
// Public pages
|
||||||
|
home -> services_page [color="#2E7D32"]
|
||||||
|
services_page -> cart_page [color="#2E7D32"]
|
||||||
|
cart_page -> login_page [label="if not auth", color="#2E7D32", style=dashed]
|
||||||
|
login_page -> profile_page [color="#2E7D32"]
|
||||||
|
profile_page -> pets_page [color="#2E7D32"]
|
||||||
|
profile_page -> requests_page [color="#2E7D32"]
|
||||||
|
|
||||||
|
// Backoffice pages
|
||||||
|
admin_dash -> admin_visits [color="#E65100"]
|
||||||
|
admin_dash -> admin_pets [color="#E65100"]
|
||||||
|
admin_dash -> admin_requests [color="#E65100"]
|
||||||
|
admin_dash -> admin_calendar [color="#E65100"]
|
||||||
|
|
||||||
|
// Components used by pages
|
||||||
|
admin_visits -> visits_section [color="#7B1FA2", style=dashed]
|
||||||
|
admin_visits -> drawer [color="#7B1FA2", style=dashed]
|
||||||
|
admin_dash -> sidebar [color="#7B1FA2", style=dashed]
|
||||||
|
admin_dash -> navbar [color="#7B1FA2", style=dashed]
|
||||||
|
|
||||||
|
// Services to APIs
|
||||||
|
http_service -> auth_api [color="#C62828"]
|
||||||
|
http_service -> visits_api [color="#C62828"]
|
||||||
|
http_service -> orders_api [color="#C62828"]
|
||||||
|
http_service -> petowners_api [color="#C62828"]
|
||||||
|
http_service -> vets_api [color="#C62828"]
|
||||||
|
http_service -> services_api [color="#C62828"]
|
||||||
|
http_service -> cart_api [color="#C62828"]
|
||||||
|
|
||||||
|
// Services to Backend
|
||||||
|
auth_api -> api_auth [label="POST /api/token/", color="#455A64"]
|
||||||
|
visits_api -> api_mascotas [label="CRUD /vet-visits/", color="#455A64"]
|
||||||
|
orders_api -> api_solicitudes [label="CRUD /service-requests/", color="#455A64"]
|
||||||
|
petowners_api -> api_mascotas [label="CRUD /pet-owners/", color="#455A64"]
|
||||||
|
vets_api -> api_mascotas [label="GET /veterinarians/", color="#455A64"]
|
||||||
|
services_api -> api_productos [label="GET /services/", color="#455A64"]
|
||||||
|
cart_api -> api_productos [label="CRUD /cart/", color="#455A64"]
|
||||||
|
|
||||||
|
// State connections
|
||||||
|
auth_slice -> redux_store [color="#00838F"]
|
||||||
|
visits_slice -> redux_store [color="#00838F"]
|
||||||
|
cart_slice -> redux_store [color="#00838F"]
|
||||||
|
auth_context -> redux_store [color="#00838F", style=dashed]
|
||||||
|
|
||||||
|
// User access paths
|
||||||
|
petowner_user -> home [label="public access", color="#AD1457"]
|
||||||
|
petowner_user -> profile_page [label="authenticated", color="#AD1457", style=dashed]
|
||||||
|
vet_user -> admin_dash [label="backoffice", color="#AD1457"]
|
||||||
|
admin_user -> admin_dash [label="full access", color="#AD1457"]
|
||||||
|
|
||||||
|
// API routing through HttpService
|
||||||
|
http_service -> api_auth [label="JWT refresh", color="#455A64", style=dashed]
|
||||||
|
}
|
||||||
595
cfg/amar/soleprint/atlas/books/arch/02-frontend-architecture.svg
Normal file
595
cfg/amar/soleprint/atlas/books/arch/02-frontend-architecture.svg
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.0.5 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: FrontendArchitecture Pages: 1 -->
|
||||||
|
<svg width="2391pt" height="715pt"
|
||||||
|
viewBox="0.00 0.00 2391.00 715.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 710.85)">
|
||||||
|
<title>FrontendArchitecture</title>
|
||||||
|
<polygon fill="white" stroke="none" points="-4,4 -4,-710.85 2387,-710.85 2387,4 -4,4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1191.5" y="-687.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas - Frontend Architecture (Next.js)</text>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_nextjs</title>
|
||||||
|
<path fill="#e3f2fd" stroke="#1565c0" d="M20,-452.8C20,-452.8 230,-452.8 230,-452.8 236,-452.8 242,-458.8 242,-464.8 242,-464.8 242,-634.32 242,-634.32 242,-640.32 236,-646.32 230,-646.32 230,-646.32 20,-646.32 20,-646.32 14,-646.32 8,-640.32 8,-634.32 8,-634.32 8,-464.8 8,-464.8 8,-458.8 14,-452.8 20,-452.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="125" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Next.js 13+ (App Router)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_public</title>
|
||||||
|
<path fill="#e8f5e9" stroke="#2e7d32" d="M262,-8C262,-8 454,-8 454,-8 460,-8 466,-14 466,-20 466,-20 466,-520 466,-520 466,-526 460,-532 454,-532 454,-532 262,-532 262,-532 256,-532 250,-526 250,-520 250,-520 250,-20 250,-20 250,-14 256,-8 262,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="358" y="-512.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Public Frontend (/(frontend))</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust3" class="cluster">
|
||||||
|
<title>cluster_backoffice</title>
|
||||||
|
<path fill="#fff3e0" stroke="#e65100" d="M647,-344.8C647,-344.8 1095,-344.8 1095,-344.8 1101,-344.8 1107,-350.8 1107,-356.8 1107,-356.8 1107,-520 1107,-520 1107,-526 1101,-532 1095,-532 1095,-532 647,-532 647,-532 641,-532 635,-526 635,-520 635,-520 635,-356.8 635,-356.8 635,-350.8 641,-344.8 647,-344.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="871" y="-512.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Backoffice (/(backoffice)/admin)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust4" class="cluster">
|
||||||
|
<title>cluster_components</title>
|
||||||
|
<path fill="#f3e5f5" stroke="#7b1fa2" d="M486,-236.8C486,-236.8 1091,-236.8 1091,-236.8 1097,-236.8 1103,-242.8 1103,-248.8 1103,-248.8 1103,-304 1103,-304 1103,-310 1097,-316 1091,-316 1091,-316 486,-316 486,-316 480,-316 474,-310 474,-304 474,-304 474,-248.8 474,-248.8 474,-242.8 480,-236.8 486,-236.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="788.5" y="-296.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Shared Components (/components)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust5" class="cluster">
|
||||||
|
<title>cluster_services</title>
|
||||||
|
<path fill="#ffebee" stroke="#c62828" d="M1222,-452.8C1222,-452.8 1922,-452.8 1922,-452.8 1928,-452.8 1934,-458.8 1934,-464.8 1934,-464.8 1934,-634.32 1934,-634.32 1934,-640.32 1928,-646.32 1922,-646.32 1922,-646.32 1222,-646.32 1222,-646.32 1216,-646.32 1210,-640.32 1210,-634.32 1210,-634.32 1210,-464.8 1210,-464.8 1210,-458.8 1216,-452.8 1222,-452.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1572" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services Layer (/services)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust6" class="cluster">
|
||||||
|
<title>cluster_state</title>
|
||||||
|
<path fill="#e0f7fa" stroke="#00838f" d="M2031,-452.8C2031,-452.8 2363,-452.8 2363,-452.8 2369,-452.8 2375,-458.8 2375,-464.8 2375,-464.8 2375,-634.32 2375,-634.32 2375,-640.32 2369,-646.32 2363,-646.32 2363,-646.32 2031,-646.32 2031,-646.32 2025,-646.32 2019,-640.32 2019,-634.32 2019,-634.32 2019,-464.8 2019,-464.8 2019,-458.8 2025,-452.8 2031,-452.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2197" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">State Management (/redux, /contexts)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust7" class="cluster">
|
||||||
|
<title>cluster_backend</title>
|
||||||
|
<path fill="#eceff1" stroke="#455a64" d="M1382,-344.8C1382,-344.8 1892,-344.8 1892,-344.8 1898,-344.8 1904,-350.8 1904,-356.8 1904,-356.8 1904,-412 1904,-412 1904,-418 1898,-424 1892,-424 1892,-424 1382,-424 1382,-424 1376,-424 1370,-418 1370,-412 1370,-412 1370,-356.8 1370,-356.8 1370,-350.8 1376,-344.8 1382,-344.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1637" y="-404.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Django Backend API</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust8" class="cluster">
|
||||||
|
<title>cluster_users</title>
|
||||||
|
<path fill="#fce4ec" stroke="#ad1457" d="M824,-560.8C824,-560.8 1158,-560.8 1158,-560.8 1164,-560.8 1170,-566.8 1170,-572.8 1170,-572.8 1170,-640.65 1170,-640.65 1170,-646.65 1164,-652.65 1158,-652.65 1158,-652.65 824,-652.65 824,-652.65 818,-652.65 812,-646.65 812,-640.65 812,-640.65 812,-572.8 812,-572.8 812,-566.8 818,-560.8 824,-560.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="991" y="-633.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">User Types</text>
|
||||||
|
</g>
|
||||||
|
<!-- app_router -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>app_router</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M155.82,-611.12C155.82,-611.12 108.18,-611.12 108.18,-611.12 102.18,-611.12 96.18,-605.12 96.18,-599.12 96.18,-599.12 96.18,-587.12 96.18,-587.12 96.18,-581.12 102.18,-575.12 108.18,-575.12 108.18,-575.12 155.82,-575.12 155.82,-575.12 161.82,-575.12 167.82,-581.12 167.82,-587.12 167.82,-587.12 167.82,-599.12 167.82,-599.12 167.82,-605.12 161.82,-611.12 155.82,-611.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="132" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">App Router</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="132" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">/app/*</text>
|
||||||
|
</g>
|
||||||
|
<!-- layout -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>layout</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M118.23,-496.8C118.23,-496.8 27.77,-496.8 27.77,-496.8 21.77,-496.8 15.77,-490.8 15.77,-484.8 15.77,-484.8 15.77,-472.8 15.77,-472.8 15.77,-466.8 21.77,-460.8 27.77,-460.8 27.77,-460.8 118.23,-460.8 118.23,-460.8 124.23,-460.8 130.23,-466.8 130.23,-472.8 130.23,-472.8 130.23,-484.8 130.23,-484.8 130.23,-490.8 124.23,-496.8 118.23,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="73" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Layout Components</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="73" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(RootLayout, etc)</text>
|
||||||
|
</g>
|
||||||
|
<!-- app_router->layout -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>app_router->layout</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M113.2,-574.76C113.2,-574.76 113.2,-508.64 113.2,-508.64"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="116.7,-508.64 113.2,-498.64 109.7,-508.64 116.7,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- middleware -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>middleware</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M221.84,-496.8C221.84,-496.8 160.16,-496.8 160.16,-496.8 154.16,-496.8 148.16,-490.8 148.16,-484.8 148.16,-484.8 148.16,-472.8 148.16,-472.8 148.16,-466.8 154.16,-460.8 160.16,-460.8 160.16,-460.8 221.84,-460.8 221.84,-460.8 227.84,-460.8 233.84,-466.8 233.84,-472.8 233.84,-472.8 233.84,-484.8 233.84,-484.8 233.84,-490.8 227.84,-496.8 221.84,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="191" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Middleware</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="191" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Auth redirect)</text>
|
||||||
|
</g>
|
||||||
|
<!-- app_router->middleware -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>app_router->middleware</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M157.99,-574.76C157.99,-574.76 157.99,-508.64 157.99,-508.64"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="161.49,-508.64 157.99,-498.64 154.49,-508.64 161.49,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- home -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>home</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M442.04,-496.8C442.04,-496.8 391.96,-496.8 391.96,-496.8 385.96,-496.8 379.96,-490.8 379.96,-484.8 379.96,-484.8 379.96,-472.8 379.96,-472.8 379.96,-466.8 385.96,-460.8 391.96,-460.8 391.96,-460.8 442.04,-460.8 442.04,-460.8 448.04,-460.8 454.04,-466.8 454.04,-472.8 454.04,-472.8 454.04,-484.8 454.04,-484.8 454.04,-490.8 448.04,-496.8 442.04,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="417" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Home Page</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="417" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">/</text>
|
||||||
|
</g>
|
||||||
|
<!-- services_page -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>services_page</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M445.57,-388.8C445.57,-388.8 370.43,-388.8 370.43,-388.8 364.43,-388.8 358.43,-382.8 358.43,-376.8 358.43,-376.8 358.43,-364.8 358.43,-364.8 358.43,-358.8 364.43,-352.8 370.43,-352.8 370.43,-352.8 445.57,-352.8 445.57,-352.8 451.57,-352.8 457.57,-358.8 457.57,-364.8 457.57,-364.8 457.57,-376.8 457.57,-376.8 457.57,-382.8 451.57,-388.8 445.57,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="408" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Services Catalog</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="408" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/servicios</text>
|
||||||
|
</g>
|
||||||
|
<!-- home->services_page -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>home->services_page</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M417,-460.48C417,-460.48 417,-400.72 417,-400.72"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="420.5,-400.72 417,-390.72 413.5,-400.72 420.5,-400.72"/>
|
||||||
|
</g>
|
||||||
|
<!-- cart_page -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>cart_page</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M425,-280.8C425,-280.8 395,-280.8 395,-280.8 389,-280.8 383,-274.8 383,-268.8 383,-268.8 383,-256.8 383,-256.8 383,-250.8 389,-244.8 395,-244.8 395,-244.8 425,-244.8 425,-244.8 431,-244.8 437,-250.8 437,-256.8 437,-256.8 437,-268.8 437,-268.8 437,-274.8 431,-280.8 425,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Cart</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">/carrito</text>
|
||||||
|
</g>
|
||||||
|
<!-- services_page->cart_page -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>services_page->cart_page</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M410,-352.48C410,-352.48 410,-292.72 410,-292.72"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="413.5,-292.72 410,-282.72 406.5,-292.72 413.5,-292.72"/>
|
||||||
|
</g>
|
||||||
|
<!-- login_page -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>login_page</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M444.47,-198C444.47,-198 381.53,-198 381.53,-198 375.53,-198 369.53,-192 369.53,-186 369.53,-186 369.53,-174 369.53,-174 369.53,-168 375.53,-162 381.53,-162 381.53,-162 444.47,-162 444.47,-162 450.47,-162 456.47,-168 456.47,-174 456.47,-174 456.47,-186 456.47,-186 456.47,-192 450.47,-198 444.47,-198"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="413" y="-183.3" font-family="Helvetica,sans-Serif" font-size="11.00">Login/Register</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="413" y="-170.1" font-family="Helvetica,sans-Serif" font-size="11.00">/login</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart_page->login_page -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>cart_page->login_page</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M410,-244.42C410,-244.42 410,-209.84 410,-209.84"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="413.5,-209.84 410,-199.84 406.5,-209.84 413.5,-209.84"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="432.76" y="-218.7" font-family="Helvetica,sans-Serif" font-size="9.00">if not auth</text>
|
||||||
|
</g>
|
||||||
|
<!-- profile_page -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>profile_page</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M438.73,-125C438.73,-125 389.27,-125 389.27,-125 383.27,-125 377.27,-119 377.27,-113 377.27,-113 377.27,-101 377.27,-101 377.27,-95 383.27,-89 389.27,-89 389.27,-89 438.73,-89 438.73,-89 444.73,-89 450.73,-95 450.73,-101 450.73,-101 450.73,-113 450.73,-113 450.73,-119 444.73,-125 438.73,-125"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="414" y="-110.3" font-family="Helvetica,sans-Serif" font-size="11.00">User Profile</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="414" y="-97.1" font-family="Helvetica,sans-Serif" font-size="11.00">/perfil</text>
|
||||||
|
</g>
|
||||||
|
<!-- login_page->profile_page -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>login_page->profile_page</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M414,-161.58C414,-161.58 414,-136.93 414,-136.93"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="417.5,-136.93 414,-126.93 410.5,-136.93 417.5,-136.93"/>
|
||||||
|
</g>
|
||||||
|
<!-- pets_page -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>pets_page</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M347.06,-52C347.06,-52 304.94,-52 304.94,-52 298.94,-52 292.94,-46 292.94,-40 292.94,-40 292.94,-28 292.94,-28 292.94,-22 298.94,-16 304.94,-16 304.94,-16 347.06,-16 347.06,-16 353.06,-16 359.06,-22 359.06,-28 359.06,-28 359.06,-40 359.06,-40 359.06,-46 353.06,-52 347.06,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="326" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">My Pets</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="326" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/mascotas</text>
|
||||||
|
</g>
|
||||||
|
<!-- profile_page->pets_page -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>profile_page->pets_page</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M376.8,-107C352.66,-107 325.68,-107 325.68,-107 325.68,-107 325.68,-63.93 325.68,-63.93"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="329.18,-63.93 325.68,-53.93 322.18,-63.93 329.18,-63.93"/>
|
||||||
|
</g>
|
||||||
|
<!-- requests_page -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>requests_page</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M445.09,-52C445.09,-52 388.91,-52 388.91,-52 382.91,-52 376.91,-46 376.91,-40 376.91,-40 376.91,-28 376.91,-28 376.91,-22 382.91,-16 388.91,-16 388.91,-16 445.09,-16 445.09,-16 451.09,-16 457.09,-22 457.09,-28 457.09,-28 457.09,-40 457.09,-40 457.09,-46 451.09,-52 445.09,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="417" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">My Requests</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="417" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/solicitudes</text>
|
||||||
|
</g>
|
||||||
|
<!-- profile_page->requests_page -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>profile_page->requests_page</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M414,-88.58C414,-88.58 414,-63.93 414,-63.93"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="417.5,-63.93 414,-53.93 410.5,-63.93 417.5,-63.93"/>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>admin_dash</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M892.91,-496.8C892.91,-496.8 847.09,-496.8 847.09,-496.8 841.09,-496.8 835.09,-490.8 835.09,-484.8 835.09,-484.8 835.09,-472.8 835.09,-472.8 835.09,-466.8 841.09,-460.8 847.09,-460.8 847.09,-460.8 892.91,-460.8 892.91,-460.8 898.91,-460.8 904.91,-466.8 904.91,-472.8 904.91,-472.8 904.91,-484.8 904.91,-484.8 904.91,-490.8 898.91,-496.8 892.91,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="870" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Dashboard</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="870" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_visits -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>admin_visits</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M740.77,-388.8C740.77,-388.8 655.23,-388.8 655.23,-388.8 649.23,-388.8 643.23,-382.8 643.23,-376.8 643.23,-376.8 643.23,-364.8 643.23,-364.8 643.23,-358.8 649.23,-352.8 655.23,-352.8 655.23,-352.8 740.77,-352.8 740.77,-352.8 746.77,-352.8 752.77,-358.8 752.77,-364.8 752.77,-364.8 752.77,-376.8 752.77,-376.8 752.77,-382.8 746.77,-388.8 740.77,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="698" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Visits Management</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="698" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/visits</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash->admin_visits -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>admin_dash->admin_visits</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M834.78,-468C784.46,-468 698,-468 698,-468 698,-468 698,-400.6 698,-400.6"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="701.5,-400.6 698,-390.6 694.5,-400.6 701.5,-400.6"/>
|
||||||
|
</g>
|
||||||
|
<!-- admin_pets -->
|
||||||
|
<g id="node13" class="node">
|
||||||
|
<title>admin_pets</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M845.45,-388.8C845.45,-388.8 782.55,-388.8 782.55,-388.8 776.55,-388.8 770.55,-382.8 770.55,-376.8 770.55,-376.8 770.55,-364.8 770.55,-364.8 770.55,-358.8 776.55,-352.8 782.55,-352.8 782.55,-352.8 845.45,-352.8 845.45,-352.8 851.45,-352.8 857.45,-358.8 857.45,-364.8 857.45,-364.8 857.45,-376.8 857.45,-376.8 857.45,-382.8 851.45,-388.8 845.45,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="814" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Pets Overview</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="814" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/pets</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash->admin_pets -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>admin_dash->admin_pets</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M846.27,-460.48C846.27,-460.48 846.27,-400.72 846.27,-400.72"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="849.77,-400.72 846.27,-390.72 842.77,-400.72 849.77,-400.72"/>
|
||||||
|
</g>
|
||||||
|
<!-- admin_requests -->
|
||||||
|
<g id="node14" class="node">
|
||||||
|
<title>admin_requests</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M966.72,-388.8C966.72,-388.8 887.28,-388.8 887.28,-388.8 881.28,-388.8 875.28,-382.8 875.28,-376.8 875.28,-376.8 875.28,-364.8 875.28,-364.8 875.28,-358.8 881.28,-352.8 887.28,-352.8 887.28,-352.8 966.72,-352.8 966.72,-352.8 972.72,-352.8 978.72,-358.8 978.72,-364.8 978.72,-364.8 978.72,-376.8 978.72,-376.8 978.72,-382.8 972.72,-388.8 966.72,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="927" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Service Requests</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="927" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/solicitudes</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash->admin_requests -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>admin_dash->admin_requests</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M890.1,-460.48C890.1,-460.48 890.1,-400.72 890.1,-400.72"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="893.6,-400.72 890.1,-390.72 886.6,-400.72 893.6,-400.72"/>
|
||||||
|
</g>
|
||||||
|
<!-- admin_calendar -->
|
||||||
|
<g id="node15" class="node">
|
||||||
|
<title>admin_calendar</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M1087.41,-388.8C1087.41,-388.8 1008.59,-388.8 1008.59,-388.8 1002.59,-388.8 996.59,-382.8 996.59,-376.8 996.59,-376.8 996.59,-364.8 996.59,-364.8 996.59,-358.8 1002.59,-352.8 1008.59,-352.8 1008.59,-352.8 1087.41,-352.8 1087.41,-352.8 1093.41,-352.8 1099.41,-358.8 1099.41,-364.8 1099.41,-364.8 1099.41,-376.8 1099.41,-376.8 1099.41,-382.8 1093.41,-388.8 1087.41,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1048" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Calendar View</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1048" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/calendario</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash->admin_calendar -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>admin_dash->admin_calendar</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M905.19,-470C949.1,-470 1018.5,-470 1018.5,-470 1018.5,-470 1018.5,-400.51 1018.5,-400.51"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1022,-400.51 1018.5,-390.51 1015,-400.51 1022,-400.51"/>
|
||||||
|
</g>
|
||||||
|
<!-- sidebar -->
|
||||||
|
<g id="node16" class="node">
|
||||||
|
<title>sidebar</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M537.99,-280.8C537.99,-280.8 494.01,-280.8 494.01,-280.8 488.01,-280.8 482.01,-274.8 482.01,-268.8 482.01,-268.8 482.01,-256.8 482.01,-256.8 482.01,-250.8 488.01,-244.8 494.01,-244.8 494.01,-244.8 537.99,-244.8 537.99,-244.8 543.99,-244.8 549.99,-250.8 549.99,-256.8 549.99,-256.8 549.99,-268.8 549.99,-268.8 549.99,-274.8 543.99,-280.8 537.99,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="516" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Sidebar</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="516" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Navigation</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash->sidebar -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>admin_dash->sidebar</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M834.76,-482C745.22,-482 516,-482 516,-482 516,-482 516,-292.63 516,-292.63"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="519.5,-292.63 516,-282.63 512.5,-292.63 519.5,-292.63"/>
|
||||||
|
</g>
|
||||||
|
<!-- navbar -->
|
||||||
|
<g id="node17" class="node">
|
||||||
|
<title>navbar</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M658.1,-280.8C658.1,-280.8 579.9,-280.8 579.9,-280.8 573.9,-280.8 567.9,-274.8 567.9,-268.8 567.9,-268.8 567.9,-256.8 567.9,-256.8 567.9,-250.8 573.9,-244.8 579.9,-244.8 579.9,-244.8 658.1,-244.8 658.1,-244.8 664.1,-244.8 670.1,-250.8 670.1,-256.8 670.1,-256.8 670.1,-268.8 670.1,-268.8 670.1,-274.8 664.1,-280.8 658.1,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="619" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">NavbarBackoffice</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="619" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Top Bar</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_dash->navbar -->
|
||||||
|
<g id="edge16" class="edge">
|
||||||
|
<title>admin_dash->navbar</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M834.61,-475C762.55,-475 605.56,-475 605.56,-475 605.56,-475 605.56,-292.77 605.56,-292.77"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="609.06,-292.77 605.56,-282.77 602.06,-292.77 609.06,-292.77"/>
|
||||||
|
</g>
|
||||||
|
<!-- visits_section -->
|
||||||
|
<g id="node18" class="node">
|
||||||
|
<title>visits_section</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M765.53,-280.8C765.53,-280.8 700.47,-280.8 700.47,-280.8 694.47,-280.8 688.47,-274.8 688.47,-268.8 688.47,-268.8 688.47,-256.8 688.47,-256.8 688.47,-250.8 694.47,-244.8 700.47,-244.8 700.47,-244.8 765.53,-244.8 765.53,-244.8 771.53,-244.8 777.53,-250.8 777.53,-256.8 777.53,-256.8 777.53,-268.8 777.53,-268.8 777.53,-274.8 771.53,-280.8 765.53,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="733" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">VisitsSection</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="733" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(List + Actions)</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_visits->visits_section -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>admin_visits->visits_section</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M709.91,-352.48C709.91,-352.48 709.91,-292.72 709.91,-292.72"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="713.41,-292.72 709.91,-282.72 706.41,-292.72 713.41,-292.72"/>
|
||||||
|
</g>
|
||||||
|
<!-- drawer -->
|
||||||
|
<g id="node19" class="node">
|
||||||
|
<title>drawer</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M860.86,-280.8C860.86,-280.8 807.14,-280.8 807.14,-280.8 801.14,-280.8 795.14,-274.8 795.14,-268.8 795.14,-268.8 795.14,-256.8 795.14,-256.8 795.14,-250.8 801.14,-244.8 807.14,-244.8 807.14,-244.8 860.86,-244.8 860.86,-244.8 866.86,-244.8 872.86,-250.8 872.86,-256.8 872.86,-256.8 872.86,-268.8 872.86,-268.8 872.86,-274.8 866.86,-280.8 860.86,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="834" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">VisitsDrawer</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="834" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Side Panel)</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_visits->drawer -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>admin_visits->drawer</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M731.34,-352.59C731.34,-336.98 731.34,-317 731.34,-317 731.34,-317 826.29,-317 826.29,-317 826.29,-317 826.29,-292.66 826.29,-292.66"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="829.79,-292.66 826.29,-282.66 822.79,-292.66 829.79,-292.66"/>
|
||||||
|
</g>
|
||||||
|
<!-- tables -->
|
||||||
|
<g id="node20" class="node">
|
||||||
|
<title>tables</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M957.18,-280.8C957.18,-280.8 902.82,-280.8 902.82,-280.8 896.82,-280.8 890.82,-274.8 890.82,-268.8 890.82,-268.8 890.82,-256.8 890.82,-256.8 890.82,-250.8 896.82,-244.8 902.82,-244.8 902.82,-244.8 957.18,-244.8 957.18,-244.8 963.18,-244.8 969.18,-250.8 969.18,-256.8 969.18,-256.8 969.18,-268.8 969.18,-268.8 969.18,-274.8 963.18,-280.8 957.18,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="930" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">DataTable</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="930" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Components</text>
|
||||||
|
</g>
|
||||||
|
<!-- forms -->
|
||||||
|
<g id="node21" class="node">
|
||||||
|
<title>forms</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M1082.54,-280.8C1082.54,-280.8 999.46,-280.8 999.46,-280.8 993.46,-280.8 987.46,-274.8 987.46,-268.8 987.46,-268.8 987.46,-256.8 987.46,-256.8 987.46,-250.8 993.46,-244.8 999.46,-244.8 999.46,-244.8 1082.54,-244.8 1082.54,-244.8 1088.54,-244.8 1094.54,-250.8 1094.54,-256.8 1094.54,-256.8 1094.54,-268.8 1094.54,-268.8 1094.54,-274.8 1088.54,-280.8 1082.54,-280.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1041" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Form Components</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1041" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Pet, Visit, etc)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service -->
|
||||||
|
<g id="node22" class="node">
|
||||||
|
<title>http_service</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1644.51,-611.12C1644.51,-611.12 1575.49,-611.12 1575.49,-611.12 1569.49,-611.12 1563.49,-605.12 1563.49,-599.12 1563.49,-599.12 1563.49,-587.12 1563.49,-587.12 1563.49,-581.12 1569.49,-575.12 1575.49,-575.12 1575.49,-575.12 1644.51,-575.12 1644.51,-575.12 1650.51,-575.12 1656.51,-581.12 1656.51,-587.12 1656.51,-587.12 1656.51,-599.12 1656.51,-599.12 1656.51,-605.12 1650.51,-611.12 1644.51,-611.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1610" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">HttpService</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1610" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Axios wrapper)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_api -->
|
||||||
|
<g id="node23" class="node">
|
||||||
|
<title>auth_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1914.15,-496.8C1914.15,-496.8 1851.85,-496.8 1851.85,-496.8 1845.85,-496.8 1839.85,-490.8 1839.85,-484.8 1839.85,-484.8 1839.85,-472.8 1839.85,-472.8 1839.85,-466.8 1845.85,-460.8 1851.85,-460.8 1851.85,-460.8 1914.15,-460.8 1914.15,-460.8 1920.15,-460.8 1926.15,-466.8 1926.15,-472.8 1926.15,-472.8 1926.15,-484.8 1926.15,-484.8 1926.15,-490.8 1920.15,-496.8 1914.15,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1883" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">authAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1883" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(login/register)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->auth_api -->
|
||||||
|
<g id="edge17" class="edge">
|
||||||
|
<title>http_service->auth_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1656.93,-602C1734.99,-602 1883,-602 1883,-602 1883,-602 1883,-508.76 1883,-508.76"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1886.5,-508.76 1883,-498.76 1879.5,-508.76 1886.5,-508.76"/>
|
||||||
|
</g>
|
||||||
|
<!-- visits_api -->
|
||||||
|
<g id="node24" class="node">
|
||||||
|
<title>visits_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1810.3,-496.8C1810.3,-496.8 1751.7,-496.8 1751.7,-496.8 1745.7,-496.8 1739.7,-490.8 1739.7,-484.8 1739.7,-484.8 1739.7,-472.8 1739.7,-472.8 1739.7,-466.8 1745.7,-460.8 1751.7,-460.8 1751.7,-460.8 1810.3,-460.8 1810.3,-460.8 1816.3,-460.8 1822.3,-466.8 1822.3,-472.8 1822.3,-472.8 1822.3,-484.8 1822.3,-484.8 1822.3,-490.8 1816.3,-496.8 1810.3,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1781" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">visitsAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1781" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(CRUD visits)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->visits_api -->
|
||||||
|
<g id="edge18" class="edge">
|
||||||
|
<title>http_service->visits_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1656.73,-593C1707.36,-593 1781,-593 1781,-593 1781,-593 1781,-508.66 1781,-508.66"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1784.5,-508.66 1781,-498.66 1777.5,-508.66 1784.5,-508.66"/>
|
||||||
|
</g>
|
||||||
|
<!-- orders_api -->
|
||||||
|
<g id="node25" class="node">
|
||||||
|
<title>orders_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1309.71,-496.8C1309.71,-496.8 1230.29,-496.8 1230.29,-496.8 1224.29,-496.8 1218.29,-490.8 1218.29,-484.8 1218.29,-484.8 1218.29,-472.8 1218.29,-472.8 1218.29,-466.8 1224.29,-460.8 1230.29,-460.8 1230.29,-460.8 1309.71,-460.8 1309.71,-460.8 1315.71,-460.8 1321.71,-466.8 1321.71,-472.8 1321.71,-472.8 1321.71,-484.8 1321.71,-484.8 1321.71,-490.8 1315.71,-496.8 1309.71,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1270" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">OrdersAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1270" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(service requests)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->orders_api -->
|
||||||
|
<g id="edge19" class="edge">
|
||||||
|
<title>http_service->orders_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1563.08,-602C1469.81,-602 1270,-602 1270,-602 1270,-602 1270,-508.76 1270,-508.76"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1273.5,-508.76 1270,-498.76 1266.5,-508.76 1273.5,-508.76"/>
|
||||||
|
</g>
|
||||||
|
<!-- petowners_api -->
|
||||||
|
<g id="node26" class="node">
|
||||||
|
<title>petowners_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1709.46,-496.8C1709.46,-496.8 1646.54,-496.8 1646.54,-496.8 1640.54,-496.8 1634.54,-490.8 1634.54,-484.8 1634.54,-484.8 1634.54,-472.8 1634.54,-472.8 1634.54,-466.8 1640.54,-460.8 1646.54,-460.8 1646.54,-460.8 1709.46,-460.8 1709.46,-460.8 1715.46,-460.8 1721.46,-466.8 1721.46,-472.8 1721.46,-472.8 1721.46,-484.8 1721.46,-484.8 1721.46,-490.8 1715.46,-496.8 1709.46,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1678" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">petOwnersAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1678" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(clients)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->petowners_api -->
|
||||||
|
<g id="edge20" class="edge">
|
||||||
|
<title>http_service->petowners_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1645.53,-574.76C1645.53,-574.76 1645.53,-508.64 1645.53,-508.64"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1649.03,-508.64 1645.53,-498.64 1642.03,-508.64 1649.03,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- vets_api -->
|
||||||
|
<g id="node27" class="node">
|
||||||
|
<title>vets_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1604.27,-496.8C1604.27,-496.8 1529.73,-496.8 1529.73,-496.8 1523.73,-496.8 1517.73,-490.8 1517.73,-484.8 1517.73,-484.8 1517.73,-472.8 1517.73,-472.8 1517.73,-466.8 1523.73,-460.8 1529.73,-460.8 1529.73,-460.8 1604.27,-460.8 1604.27,-460.8 1610.27,-460.8 1616.27,-466.8 1616.27,-472.8 1616.27,-472.8 1616.27,-484.8 1616.27,-484.8 1616.27,-490.8 1610.27,-496.8 1604.27,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1567" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">VeterinariansAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1567" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(professionals)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->vets_api -->
|
||||||
|
<g id="edge21" class="edge">
|
||||||
|
<title>http_service->vets_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1589.88,-574.76C1589.88,-574.76 1589.88,-508.64 1589.88,-508.64"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1593.38,-508.64 1589.88,-498.64 1586.38,-508.64 1593.38,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- services_api -->
|
||||||
|
<g id="node28" class="node">
|
||||||
|
<title>services_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1488.04,-496.8C1488.04,-496.8 1437.96,-496.8 1437.96,-496.8 1431.96,-496.8 1425.96,-490.8 1425.96,-484.8 1425.96,-484.8 1425.96,-472.8 1425.96,-472.8 1425.96,-466.8 1431.96,-460.8 1437.96,-460.8 1437.96,-460.8 1488.04,-460.8 1488.04,-460.8 1494.04,-460.8 1500.04,-466.8 1500.04,-472.8 1500.04,-472.8 1500.04,-484.8 1500.04,-484.8 1500.04,-490.8 1494.04,-496.8 1488.04,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1463" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">servicesAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1463" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(catalog)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->services_api -->
|
||||||
|
<g id="edge22" class="edge">
|
||||||
|
<title>http_service->services_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1563.08,-584C1520.06,-584 1463,-584 1463,-584 1463,-584 1463,-508.64 1463,-508.64"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1466.5,-508.64 1463,-498.64 1459.5,-508.64 1466.5,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- cart_api -->
|
||||||
|
<g id="node29" class="node">
|
||||||
|
<title>cart_api</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M1395.99,-496.8C1395.99,-496.8 1352.01,-496.8 1352.01,-496.8 1346.01,-496.8 1340.01,-490.8 1340.01,-484.8 1340.01,-484.8 1340.01,-472.8 1340.01,-472.8 1340.01,-466.8 1346.01,-460.8 1352.01,-460.8 1352.01,-460.8 1395.99,-460.8 1395.99,-460.8 1401.99,-460.8 1407.99,-466.8 1407.99,-472.8 1407.99,-472.8 1407.99,-484.8 1407.99,-484.8 1407.99,-490.8 1401.99,-496.8 1395.99,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1374" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">CartAPI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1374" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(shopping)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->cart_api -->
|
||||||
|
<g id="edge23" class="edge">
|
||||||
|
<title>http_service->cart_api</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1563.09,-593C1494.27,-593 1374,-593 1374,-593 1374,-593 1374,-508.66 1374,-508.66"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1377.5,-508.66 1374,-498.66 1370.5,-508.66 1377.5,-508.66"/>
|
||||||
|
</g>
|
||||||
|
<!-- api_auth -->
|
||||||
|
<g id="node38" class="node">
|
||||||
|
<title>api_auth</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1773.81,-388.8C1773.81,-388.8 1726.19,-388.8 1726.19,-388.8 1720.19,-388.8 1714.19,-382.8 1714.19,-376.8 1714.19,-376.8 1714.19,-364.8 1714.19,-364.8 1714.19,-358.8 1720.19,-352.8 1726.19,-352.8 1726.19,-352.8 1773.81,-352.8 1773.81,-352.8 1779.81,-352.8 1785.81,-358.8 1785.81,-364.8 1785.81,-364.8 1785.81,-376.8 1785.81,-376.8 1785.81,-382.8 1779.81,-388.8 1773.81,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1750" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/api/token/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1750" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(JWT Auth)</text>
|
||||||
|
</g>
|
||||||
|
<!-- http_service->api_auth -->
|
||||||
|
<g id="edge39" class="edge">
|
||||||
|
<title>http_service->api_auth</title>
|
||||||
|
<path fill="none" stroke="#455a64" stroke-dasharray="5,2" d="M1656.96,-584C1690.82,-584 1730.58,-584 1730.58,-584 1730.58,-584 1730.58,-400.49 1730.58,-400.49"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1734.08,-400.49 1730.58,-390.49 1727.08,-400.49 1734.08,-400.49"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1986.5" y="-476.1" font-family="Helvetica,sans-Serif" font-size="9.00">JWT refresh</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_api->api_auth -->
|
||||||
|
<g id="edge24" class="edge">
|
||||||
|
<title>auth_api->api_auth</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1867.88,-460.31C1867.88,-439.04 1867.88,-407 1867.88,-407 1867.88,-407 1770.44,-407 1770.44,-407 1770.44,-407 1770.44,-400.41 1770.44,-400.41"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1773.94,-400.41 1770.44,-390.41 1766.94,-400.41 1773.94,-400.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1909.27" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">POST /api/token/</text>
|
||||||
|
</g>
|
||||||
|
<!-- api_mascotas -->
|
||||||
|
<g id="node35" class="node">
|
||||||
|
<title>api_mascotas</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1683.93,-388.8C1683.93,-388.8 1602.07,-388.8 1602.07,-388.8 1596.07,-388.8 1590.07,-382.8 1590.07,-376.8 1590.07,-376.8 1590.07,-364.8 1590.07,-364.8 1590.07,-358.8 1596.07,-352.8 1602.07,-352.8 1602.07,-352.8 1683.93,-352.8 1683.93,-352.8 1689.93,-352.8 1695.93,-358.8 1695.93,-364.8 1695.93,-364.8 1695.93,-376.8 1695.93,-376.8 1695.93,-382.8 1689.93,-388.8 1683.93,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1643" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/mascotas/api/v1/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1643" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Pets, Vets, Visits)</text>
|
||||||
|
</g>
|
||||||
|
<!-- visits_api->api_mascotas -->
|
||||||
|
<g id="edge25" class="edge">
|
||||||
|
<title>visits_api->api_mascotas</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1755.07,-460.59C1755.07,-444.98 1755.07,-425 1755.07,-425 1755.07,-425 1675.46,-425 1675.46,-425 1675.46,-425 1675.46,-400.66 1675.46,-400.66"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1678.96,-400.66 1675.46,-390.66 1671.96,-400.66 1678.96,-400.66"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1814.25" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /vet-visits/</text>
|
||||||
|
</g>
|
||||||
|
<!-- api_solicitudes -->
|
||||||
|
<g id="node37" class="node">
|
||||||
|
<title>api_solicitudes</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1439.74,-388.8C1439.74,-388.8 1390.26,-388.8 1390.26,-388.8 1384.26,-388.8 1378.26,-382.8 1378.26,-376.8 1378.26,-376.8 1378.26,-364.8 1378.26,-364.8 1378.26,-358.8 1384.26,-352.8 1390.26,-352.8 1390.26,-352.8 1439.74,-352.8 1439.74,-352.8 1445.74,-352.8 1451.74,-358.8 1451.74,-364.8 1451.74,-364.8 1451.74,-376.8 1451.74,-376.8 1451.74,-382.8 1445.74,-388.8 1439.74,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1415" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/solicitudes/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1415" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Requests)</text>
|
||||||
|
</g>
|
||||||
|
<!-- orders_api->api_solicitudes -->
|
||||||
|
<g id="edge26" class="edge">
|
||||||
|
<title>orders_api->api_solicitudes</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1270,-460.51C1270,-429.76 1270,-371 1270,-371 1270,-371 1366.52,-371 1366.52,-371"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1366.52,-374.5 1376.52,-371 1366.52,-367.5 1366.52,-374.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1320.76" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /service-requests/</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowners_api->api_mascotas -->
|
||||||
|
<g id="edge27" class="edge">
|
||||||
|
<title>petowners_api->api_mascotas</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1655,-460.48C1655,-460.48 1655,-400.72 1655,-400.72"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1658.5,-400.72 1655,-390.72 1651.5,-400.72 1658.5,-400.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1715.01" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /pet-owners/</text>
|
||||||
|
</g>
|
||||||
|
<!-- vets_api->api_mascotas -->
|
||||||
|
<g id="edge28" class="edge">
|
||||||
|
<title>vets_api->api_mascotas</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1603.17,-460.48C1603.17,-460.48 1603.17,-400.72 1603.17,-400.72"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1606.67,-400.72 1603.17,-390.72 1599.67,-400.72 1606.67,-400.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1609.76" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">GET /veterinarians/</text>
|
||||||
|
</g>
|
||||||
|
<!-- api_productos -->
|
||||||
|
<g id="node36" class="node">
|
||||||
|
<title>api_productos</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1560.09,-388.8C1560.09,-388.8 1481.91,-388.8 1481.91,-388.8 1475.91,-388.8 1469.91,-382.8 1469.91,-376.8 1469.91,-376.8 1469.91,-364.8 1469.91,-364.8 1469.91,-358.8 1475.91,-352.8 1481.91,-352.8 1481.91,-352.8 1560.09,-352.8 1560.09,-352.8 1566.09,-352.8 1572.09,-358.8 1572.09,-364.8 1572.09,-364.8 1572.09,-376.8 1572.09,-376.8 1572.09,-382.8 1566.09,-388.8 1560.09,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1521" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/productos/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1521" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Services, Prices)</text>
|
||||||
|
</g>
|
||||||
|
<!-- services_api->api_productos -->
|
||||||
|
<g id="edge29" class="edge">
|
||||||
|
<title>services_api->api_productos</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1489.99,-460.48C1489.99,-460.48 1489.99,-400.72 1489.99,-400.72"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1493.49,-400.72 1489.99,-390.72 1486.49,-400.72 1493.49,-400.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1517.5" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">GET /services/</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart_api->api_productos -->
|
||||||
|
<g id="edge30" class="edge">
|
||||||
|
<title>cart_api->api_productos</title>
|
||||||
|
<path fill="none" stroke="#455a64" d="M1393.13,-460.48C1393.13,-451.67 1393.13,-443 1393.13,-443 1393.13,-443 1479.95,-443 1479.95,-443 1479.95,-443 1479.95,-400.53 1479.95,-400.53"/>
|
||||||
|
<polygon fill="#455a64" stroke="#455a64" points="1483.45,-400.53 1479.95,-390.53 1476.45,-400.53 1483.45,-400.53"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1433.25" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /cart/</text>
|
||||||
|
</g>
|
||||||
|
<!-- redux_store -->
|
||||||
|
<g id="node30" class="node">
|
||||||
|
<title>redux_store</title>
|
||||||
|
<path fill="#b2ebf2" stroke="black" d="M2228.93,-496.8C2228.93,-496.8 2169.07,-496.8 2169.07,-496.8 2163.07,-496.8 2157.07,-490.8 2157.07,-484.8 2157.07,-484.8 2157.07,-472.8 2157.07,-472.8 2157.07,-466.8 2163.07,-460.8 2169.07,-460.8 2169.07,-460.8 2228.93,-460.8 2228.93,-460.8 2234.93,-460.8 2240.93,-466.8 2240.93,-472.8 2240.93,-472.8 2240.93,-484.8 2240.93,-484.8 2240.93,-490.8 2234.93,-496.8 2228.93,-496.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2199" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Redux Store</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2199" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Global State)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_slice -->
|
||||||
|
<g id="node31" class="node">
|
||||||
|
<title>auth_slice</title>
|
||||||
|
<path fill="#b2ebf2" stroke="black" d="M2092.87,-611.12C2092.87,-611.12 2039.13,-611.12 2039.13,-611.12 2033.13,-611.12 2027.13,-605.12 2027.13,-599.12 2027.13,-599.12 2027.13,-587.12 2027.13,-587.12 2027.13,-581.12 2033.13,-575.12 2039.13,-575.12 2039.13,-575.12 2092.87,-575.12 2092.87,-575.12 2098.87,-575.12 2104.87,-581.12 2104.87,-587.12 2104.87,-587.12 2104.87,-599.12 2104.87,-599.12 2104.87,-605.12 2098.87,-611.12 2092.87,-611.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2066" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Auth Slice</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2066" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(user, token)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_slice->redux_store -->
|
||||||
|
<g id="edge31" class="edge">
|
||||||
|
<title>auth_slice->redux_store</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M2066,-574.79C2066,-542.53 2066,-479 2066,-479 2066,-479 2145.4,-479 2145.4,-479"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="2145.4,-482.5 2155.4,-479 2145.4,-475.5 2145.4,-482.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- visits_slice -->
|
||||||
|
<g id="node32" class="node">
|
||||||
|
<title>visits_slice</title>
|
||||||
|
<path fill="#b2ebf2" stroke="black" d="M2179.59,-611.12C2179.59,-611.12 2134.41,-611.12 2134.41,-611.12 2128.41,-611.12 2122.41,-605.12 2122.41,-599.12 2122.41,-599.12 2122.41,-587.12 2122.41,-587.12 2122.41,-581.12 2128.41,-575.12 2134.41,-575.12 2134.41,-575.12 2179.59,-575.12 2179.59,-575.12 2185.59,-575.12 2191.59,-581.12 2191.59,-587.12 2191.59,-587.12 2191.59,-599.12 2191.59,-599.12 2191.59,-605.12 2185.59,-611.12 2179.59,-611.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2157" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Visits Slice</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2157" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(visit data)</text>
|
||||||
|
</g>
|
||||||
|
<!-- visits_slice->redux_store -->
|
||||||
|
<g id="edge32" class="edge">
|
||||||
|
<title>visits_slice->redux_store</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M2174.33,-574.76C2174.33,-574.76 2174.33,-508.64 2174.33,-508.64"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="2177.83,-508.64 2174.33,-498.64 2170.83,-508.64 2177.83,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- cart_slice -->
|
||||||
|
<g id="node33" class="node">
|
||||||
|
<title>cart_slice</title>
|
||||||
|
<path fill="#b2ebf2" stroke="black" d="M2260.84,-611.12C2260.84,-611.12 2221.16,-611.12 2221.16,-611.12 2215.16,-611.12 2209.16,-605.12 2209.16,-599.12 2209.16,-599.12 2209.16,-587.12 2209.16,-587.12 2209.16,-581.12 2215.16,-575.12 2221.16,-575.12 2221.16,-575.12 2260.84,-575.12 2260.84,-575.12 2266.84,-575.12 2272.84,-581.12 2272.84,-587.12 2272.84,-587.12 2272.84,-599.12 2272.84,-599.12 2272.84,-605.12 2266.84,-611.12 2260.84,-611.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2241" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Cart Slice</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2241" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(items)</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart_slice->redux_store -->
|
||||||
|
<g id="edge33" class="edge">
|
||||||
|
<title>cart_slice->redux_store</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M2225.05,-574.76C2225.05,-574.76 2225.05,-508.64 2225.05,-508.64"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="2228.55,-508.64 2225.05,-498.64 2221.55,-508.64 2228.55,-508.64"/>
|
||||||
|
</g>
|
||||||
|
<!-- auth_context -->
|
||||||
|
<g id="node34" class="node">
|
||||||
|
<title>auth_context</title>
|
||||||
|
<path fill="#b2ebf2" stroke="black" d="M2355.27,-611.12C2355.27,-611.12 2302.73,-611.12 2302.73,-611.12 2296.73,-611.12 2290.73,-605.12 2290.73,-599.12 2290.73,-599.12 2290.73,-587.12 2290.73,-587.12 2290.73,-581.12 2296.73,-575.12 2302.73,-575.12 2302.73,-575.12 2355.27,-575.12 2355.27,-575.12 2361.27,-575.12 2367.27,-581.12 2367.27,-587.12 2367.27,-587.12 2367.27,-599.12 2367.27,-599.12 2367.27,-605.12 2361.27,-611.12 2355.27,-611.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2329" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">AuthContext</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2329" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Provider)</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_context->redux_store -->
|
||||||
|
<g id="edge34" class="edge">
|
||||||
|
<title>auth_context->redux_store</title>
|
||||||
|
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M2329,-574.79C2329,-542.53 2329,-479 2329,-479 2329,-479 2252.72,-479 2252.72,-479"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="2252.72,-475.5 2242.72,-479 2252.72,-482.5 2252.72,-475.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- api_payments -->
|
||||||
|
<g id="node39" class="node">
|
||||||
|
<title>api_payments</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1883.91,-388.8C1883.91,-388.8 1816.09,-388.8 1816.09,-388.8 1810.09,-388.8 1804.09,-382.8 1804.09,-376.8 1804.09,-376.8 1804.09,-364.8 1804.09,-364.8 1804.09,-358.8 1810.09,-352.8 1816.09,-352.8 1816.09,-352.8 1883.91,-352.8 1883.91,-352.8 1889.91,-352.8 1895.91,-358.8 1895.91,-364.8 1895.91,-364.8 1895.91,-376.8 1895.91,-376.8 1895.91,-382.8 1889.91,-388.8 1883.91,-388.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1850" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/payments/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1850" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(MercadoPago)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner_user -->
|
||||||
|
<g id="node40" class="node">
|
||||||
|
<title>petowner_user</title>
|
||||||
|
<ellipse fill="#f8bbd9" stroke="black" cx="866" cy="-593.12" rx="45.9" ry="24.32"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="866" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">PetOwner</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="866" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Cliente)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner_user->home -->
|
||||||
|
<g id="edge35" class="edge">
|
||||||
|
<title>petowner_user->home</title>
|
||||||
|
<path fill="none" stroke="#ad1457" d="M827.6,-579.22C827.6,-551.07 827.6,-490 827.6,-490 827.6,-490 466.04,-490 466.04,-490"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="466.04,-486.5 456.04,-490 466.04,-493.5 466.04,-486.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="830.01" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">public access</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner_user->profile_page -->
|
||||||
|
<g id="edge36" class="edge">
|
||||||
|
<title>petowner_user->profile_page</title>
|
||||||
|
<path fill="none" stroke="#ad1457" stroke-dasharray="5,2" d="M819.7,-593C733.92,-593 558.94,-593 558.94,-593 558.94,-593 558.94,-107 558.94,-107 558.94,-107 462.46,-107 462.46,-107"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="462.46,-103.5 452.46,-107 462.46,-110.5 462.46,-103.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1154.02" y="-326.7" font-family="Helvetica,sans-Serif" font-size="9.00">authenticated</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_user -->
|
||||||
|
<g id="node41" class="node">
|
||||||
|
<title>vet_user</title>
|
||||||
|
<ellipse fill="#f8bbd9" stroke="black" cx="985" cy="-593.12" rx="55.41" ry="24.32"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="985" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Veterinarian</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="985" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Profesional)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_user->admin_dash -->
|
||||||
|
<g id="edge37" class="edge">
|
||||||
|
<title>vet_user->admin_dash</title>
|
||||||
|
<path fill="none" stroke="#ad1457" d="M954.15,-572.61C954.15,-542.01 954.15,-488 954.15,-488 954.15,-488 916.71,-488 916.71,-488"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="916.71,-484.5 906.71,-488 916.71,-491.5 916.71,-484.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="921.26" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">backoffice</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_user -->
|
||||||
|
<g id="node42" class="node">
|
||||||
|
<title>admin_user</title>
|
||||||
|
<ellipse fill="#f8bbd9" stroke="black" cx="1110" cy="-593.12" rx="51.52" ry="24.32"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1110" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Admin/Staff</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1110" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Interno)</text>
|
||||||
|
</g>
|
||||||
|
<!-- admin_user->admin_dash -->
|
||||||
|
<g id="edge38" class="edge">
|
||||||
|
<title>admin_user->admin_dash</title>
|
||||||
|
<path fill="none" stroke="#ad1457" d="M1078.95,-573.25C1078.95,-540.55 1078.95,-479 1078.95,-479 1078.95,-479 916.49,-479 916.49,-479"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="916.49,-475.5 906.49,-479 916.49,-482.5 916.49,-475.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1181.01" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">full access</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 51 KiB |
234
cfg/amar/soleprint/atlas/books/arch/03-data-model.dot
Normal file
234
cfg/amar/soleprint/atlas/books/arch/03-data-model.dot
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
digraph DataModel {
|
||||||
|
// Graph settings
|
||||||
|
rankdir=TB
|
||||||
|
compound=true
|
||||||
|
splines=ortho
|
||||||
|
node [shape=record, style="filled", fontname="Helvetica", fontsize=10]
|
||||||
|
edge [fontname="Helvetica", fontsize=8]
|
||||||
|
|
||||||
|
label="AMAR Mascotas - Data Model (Entity Relationships)\n\n"
|
||||||
|
labelloc="t"
|
||||||
|
fontsize=16
|
||||||
|
fontname="Helvetica-Bold"
|
||||||
|
|
||||||
|
// === USERS & AUTHENTICATION ===
|
||||||
|
subgraph cluster_auth {
|
||||||
|
label="Users & Authentication"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E8F5E9"
|
||||||
|
color="#2E7D32"
|
||||||
|
|
||||||
|
auth_user [label="{auth.User|id: PK\luser name: str\lemail: str\lis_staff: bool\lis_superuser: bool\l}", fillcolor="#C8E6C9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === PET OWNERS & PETS ===
|
||||||
|
subgraph cluster_mascotas_owners {
|
||||||
|
label="Pet Owners & Pets"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E3F2FD"
|
||||||
|
color="#1565C0"
|
||||||
|
|
||||||
|
petowner [label="{PetOwner|id: PK\lemail: str (unique)\lphone: str\lfirst_name: str\llast_name: str\lneighborhood_id: FK\lcampaign_id: FK (opt)\lgeo_latitude: float\lgeo_longitude: float\laddress: str\l}", fillcolor="#BBDEFB"]
|
||||||
|
|
||||||
|
pet [label="{Pet|id: PK\lowner_id: FK\lname: str\lpet_type: DOG/CAT\lbreed_id: FK (opt)\lgender: M/F\lage: int\lweight: decimal\lheight: decimal\lbirth_date: date\lallergies: text\lneutered: bool\lis_deceased: bool\lstate: puppy/adult/...\lprofile_picture: file\l}", fillcolor="#BBDEFB"]
|
||||||
|
|
||||||
|
petvaccine [label="{PetVaccine|id: PK\lpet_id: FK\lvaccine_id: FK\lapplication_date: date\lnext_application: date\l}", fillcolor="#90CAF9"]
|
||||||
|
|
||||||
|
petstudy [label="{PetStudy|id: PK\lpet_id: FK\lstudy_id: FK\ldate: date\lresult: text\limages: files\l}", fillcolor="#90CAF9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VETERINARIANS ===
|
||||||
|
subgraph cluster_veterinarians {
|
||||||
|
label="Veterinarians"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFF3E0"
|
||||||
|
color="#E65100"
|
||||||
|
|
||||||
|
veterinarian [label="{Veterinarian|id: PK\luser_id: FK\llicense: str\lemail: str\lphone: str\l}", fillcolor="#FFE0B2"]
|
||||||
|
|
||||||
|
availability [label="{Availability|id: PK\lveterinarian_id: FK\lstart_day: 0-6\lend_day: 0-6\lstart_time: time\lend_time: time\l}", fillcolor="#FFCC80"]
|
||||||
|
|
||||||
|
unavailability [label="{Unavailability|id: PK\lveterinarian_id: FK\lstart_date: date\lend_date: date\lstart_time: time\lend_time: time\lreason: str\l}", fillcolor="#FFCC80"]
|
||||||
|
|
||||||
|
vet_specialty [label="{M2M: Vet-Specialty|veterinarian_id: FK\lspecialty_id: FK\l}", shape=diamond, fillcolor="#FFB74D"]
|
||||||
|
|
||||||
|
vet_neighborhood [label="{M2M: Vet-Neighborhood|veterinarian_id: FK\lneighborhood_id: FK\l}", shape=diamond, fillcolor="#FFB74D"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SERVICES & PRICING ===
|
||||||
|
subgraph cluster_productos {
|
||||||
|
label="Services & Pricing"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#F3E5F5"
|
||||||
|
color="#7B1FA2"
|
||||||
|
|
||||||
|
grupo [label="{Group|id: PK\lname: str\ldescription: text\l}", fillcolor="#E1BEE7"]
|
||||||
|
|
||||||
|
category [label="{Category|id: PK\lgroup_id: FK\lname: str\ldescription: text\lvalue: int (order)\l}", fillcolor="#E1BEE7"]
|
||||||
|
|
||||||
|
service [label="{Service|id: PK\lname: str\ldescription: text\lspecialty_id: FK\lcategory_id: FK\lduration: int (min)\lmodality: onsite/online\lpayment_sign_req: bool\lpet_type_filter: str\lage_filter: str\lweight_range: str\l}", fillcolor="#CE93D8"]
|
||||||
|
|
||||||
|
prices [label="{Prices|id: PK\lservice_id: FK\lveterinarian_id: FK (opt)\lprice: decimal\lprofessional_fee: decimal\lpayment_sign: decimal\lfrom_date: date\lto_date: date\lactive: bool\l}", fillcolor="#BA68C8"]
|
||||||
|
|
||||||
|
discounts [label="{Discounts|id: PK\lservice_id: FK\ldiscount: decimal (%)\lfrom_date: date\lto_date: date\lactive: bool\l}", fillcolor="#BA68C8"]
|
||||||
|
|
||||||
|
servicecombo [label="{ServiceCombo|id: PK\lname: str\ldescription: text\ldiscount_percent: decimal\ldiscount_fixed: decimal\l}", fillcolor="#E1BEE7"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CART & CHECKOUT ===
|
||||||
|
subgraph cluster_cart {
|
||||||
|
label="Cart & Checkout"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E0F7FA"
|
||||||
|
color="#00838F"
|
||||||
|
|
||||||
|
cart [label="{Cart|id: PK\lpetowner_id: FK\lveterinarian_id: FK (opt)\luse_vet_prices: bool\lapply_turn_fee: bool\l}", fillcolor="#B2EBF2"]
|
||||||
|
|
||||||
|
cartitem [label="{CartItem|id: PK\lcart_id: FK\lpet_id: FK (opt)\lservice_id: FK\lprice: decimal\lquantity: int\ltotal: decimal (calc)\l}", fillcolor="#80DEEA"]
|
||||||
|
|
||||||
|
cartresumeitem [label="{CartResumeItem|id: PK\lcart_id: FK\lconcept: SUBTOTAL/\l DESCUENTO/\l ADELANTO/\l TOTAL/\l COSTO_SERVICIO\lamount: decimal\lorder: int\l}", fillcolor="#80DEEA"]
|
||||||
|
|
||||||
|
cartpetreason [label="{CartPetReason|id: PK\lcart_id: FK\lpet_id: FK\lreason: text\l}", fillcolor="#80DEEA"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SERVICE REQUESTS ===
|
||||||
|
subgraph cluster_solicitudes {
|
||||||
|
label="Service Requests (Workflow)"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFEBEE"
|
||||||
|
color="#C62828"
|
||||||
|
|
||||||
|
servicerequest [label="{ServiceRequest|id: PK\lpetowner_id: FK\lcart_id: FK\lveterinarian_id: FK (opt)\lstate: pending/vet_asked/\l vet_accepted/coordinated/\l payed/Confirmado/...\lreason: text\ldays_requested: JSON\ldate_coordinated: datetime\lhour_coordinated: time\lpay_number: str\lcampaign_id: FK (opt)\lattended_by_id: FK (opt)\l}", fillcolor="#FFCDD2"]
|
||||||
|
|
||||||
|
statehistory [label="{StateHistory|id: PK\lservice_request_id: FK\lstate: str\ladditional_data: JSON\lcreated_at: datetime\luser_id: FK\l}", fillcolor="#EF9A9A"]
|
||||||
|
|
||||||
|
vetasked [label="{VeterinarianAsked|id: PK\lservice_request_id: FK\lveterinarian_id: FK\ldate_asked: datetime\ldate_answered: datetime\laccepted: bool\l}", fillcolor="#EF9A9A"]
|
||||||
|
|
||||||
|
vetreminder [label="{ScheduledVetReminder|id: PK\lvet_asked_id: FK\lscheduled_for: datetime\lprocessed_at: datetime\lstatus: pending/sent/...\lcelery_task_id: str\l}", fillcolor="#E57373"]
|
||||||
|
|
||||||
|
payreminder [label="{PaymentReminder|id: PK\lservice_request_id: FK\lscheduled_for: datetime\lprocessed_at: datetime\lstatus: pending/sent/...\l}", fillcolor="#E57373"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === VET VISITS ===
|
||||||
|
subgraph cluster_vetvisits {
|
||||||
|
label="Veterinary Visits"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFFDE7"
|
||||||
|
color="#F9A825"
|
||||||
|
|
||||||
|
vetvisit [label="{VetVisit|id: PK\lservice_request_id: FK (opt)\lowner_id: FK\lveterinarian_id: FK\ldate: date\lhour: time\lvisit_type: clinical/\l vaccination/\l telemedicina\lvisit_state: PENDING/\l IN_PROGRESS/\l COMPLETED/\l NO_REPORT/\l CANCELLED\lreason: text\lobservations: text\lprice: decimal\ldeposit: decimal\lvet_fee: decimal\lpay_transaction: str\lgoogle_event_id: str\lafip_receipt_id: FK (opt)\l}", fillcolor="#FFF9C4"]
|
||||||
|
|
||||||
|
vetvisitreport [label="{VetVisitReport|id: PK\lvisit_id: FK\lpet_id: FK\lreason: text\lphysical_exam: text\ldiagnosis: text\ltreatment: text\lpdf_file: file\l}", fillcolor="#FFF59D"]
|
||||||
|
|
||||||
|
vetvisitfollowup [label="{VetVisitFollowUp|id: PK\lreport_id: FK\ldate: date\ldescription: text\l}", fillcolor="#FFF176"]
|
||||||
|
|
||||||
|
vetvisitpetreason [label="{VetVisitPetReason|id: PK\lvisit_id: FK\lpet_id: FK\lreason: text\l}", fillcolor="#FFF176"]
|
||||||
|
|
||||||
|
visit_pets [label="{M2M: Visit-Pets|vetvisit_id: FK\lpet_id: FK\l}", shape=diamond, fillcolor="#FFEE58"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === REFERENCE DATA ===
|
||||||
|
subgraph cluster_reference {
|
||||||
|
label="Reference Data"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#ECEFF1"
|
||||||
|
color="#455A64"
|
||||||
|
|
||||||
|
specialty [label="{Specialty|id: PK\lname: str\l}", fillcolor="#CFD8DC"]
|
||||||
|
neighborhood [label="{Neighborhood|id: PK\lname: str\ldistance_coefficient: decimal\lcoverage_area: GIS Polygon\l}", fillcolor="#CFD8DC"]
|
||||||
|
province [label="{Province|id: PK\lname: str\l}", fillcolor="#CFD8DC"]
|
||||||
|
locality [label="{Locality|id: PK\lprovince_id: FK\lname: str\l}", fillcolor="#CFD8DC"]
|
||||||
|
petbreed [label="{PetBreed|id: PK\lname: str\lpet_type: DOG/CAT\l}", fillcolor="#CFD8DC"]
|
||||||
|
vaccine [label="{Vaccine|id: PK\lname: str\lpet_type: DOG/CAT\lperiodicity: int (months)\l}", fillcolor="#CFD8DC"]
|
||||||
|
study [label="{Study|id: PK\lname: str\lpet_type: DOG/CAT\lgroup_id: FK (opt)\l}", fillcolor="#CFD8DC"]
|
||||||
|
campaign [label="{Campaign|id: PK\lname: str\lutm_source: str\lutm_medium: str\lis_active: bool\l}", fillcolor="#CFD8DC"]
|
||||||
|
tag [label="{Tag|id: PK\lname: str (unique)\l}", fillcolor="#CFD8DC"]
|
||||||
|
medication [label="{Medication|id: PK\lname: str\lgeneral_name_id: FK\ltype_id: FK\lpresentation_id: FK\lpet_type: str\l}", fillcolor="#CFD8DC"]
|
||||||
|
turnfeegroup [label="{IndividualTurnFeeGroup|id: PK\lname: str\lfee_percentage: decimal\l}", fillcolor="#CFD8DC"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AFIP/INVOICING ===
|
||||||
|
subgraph cluster_afip {
|
||||||
|
label="AFIP Invoicing"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FCE4EC"
|
||||||
|
color="#AD1457"
|
||||||
|
|
||||||
|
receipt [label="{Receipt|id: PK\ldocument_number: bigint\lreceipt_number: int\lissued_date: date\ltotal_amount: decimal\lnet_taxed: decimal\lcae: str\lcae_expiration: date\l}", fillcolor="#F8BBD9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RELATIONSHIPS ===
|
||||||
|
|
||||||
|
// Auth -> PetOwner/Vet
|
||||||
|
auth_user -> petowner [label="1:1 opt", style=dashed, color="#2E7D32"]
|
||||||
|
auth_user -> veterinarian [label="1:1", color="#2E7D32"]
|
||||||
|
|
||||||
|
// PetOwner relationships
|
||||||
|
petowner -> pet [label="1:N owns", color="#1565C0"]
|
||||||
|
petowner -> neighborhood [label="N:1 lives in", color="#1565C0"]
|
||||||
|
petowner -> campaign [label="N:1 opt", style=dashed, color="#666"]
|
||||||
|
petowner -> cart [label="1:N", color="#00838F"]
|
||||||
|
petowner -> servicerequest [label="1:N requests", color="#C62828"]
|
||||||
|
petowner -> vetvisit [label="1:N as owner", color="#F9A825"]
|
||||||
|
|
||||||
|
// Pet relationships
|
||||||
|
pet -> petbreed [label="N:1 opt", style=dashed, color="#666"]
|
||||||
|
pet -> petvaccine [label="1:N", color="#1565C0"]
|
||||||
|
pet -> petstudy [label="1:N", color="#1565C0"]
|
||||||
|
petvaccine -> vaccine [label="N:1", color="#666"]
|
||||||
|
petstudy -> study [label="N:1", color="#666"]
|
||||||
|
|
||||||
|
// Veterinarian relationships
|
||||||
|
veterinarian -> vet_specialty [label="1:N", color="#E65100"]
|
||||||
|
vet_specialty -> specialty [label="N:1", color="#E65100"]
|
||||||
|
veterinarian -> vet_neighborhood [label="1:N coverage", color="#E65100"]
|
||||||
|
vet_neighborhood -> neighborhood [label="N:1", color="#E65100"]
|
||||||
|
veterinarian -> availability [label="1:N", color="#E65100"]
|
||||||
|
veterinarian -> unavailability [label="1:N", color="#E65100"]
|
||||||
|
veterinarian -> turnfeegroup [label="N:M", color="#E65100", style=dashed]
|
||||||
|
|
||||||
|
// Service/Pricing relationships
|
||||||
|
grupo -> category [label="1:N", color="#7B1FA2"]
|
||||||
|
category -> service [label="1:N", color="#7B1FA2"]
|
||||||
|
service -> specialty [label="N:1 opt", style=dashed, color="#7B1FA2"]
|
||||||
|
service -> prices [label="1:N", color="#7B1FA2"]
|
||||||
|
service -> discounts [label="1:N", color="#7B1FA2"]
|
||||||
|
prices -> veterinarian [label="N:1 opt\n(vet-specific)", style=dashed, color="#7B1FA2"]
|
||||||
|
|
||||||
|
// Cart relationships
|
||||||
|
cart -> veterinarian [label="N:1 opt\n(assigned vet)", style=dashed, color="#00838F"]
|
||||||
|
cart -> cartitem [label="1:N", color="#00838F"]
|
||||||
|
cart -> cartresumeitem [label="1:N", color="#00838F"]
|
||||||
|
cart -> cartpetreason [label="1:N", color="#00838F"]
|
||||||
|
cartitem -> service [label="N:1", color="#00838F"]
|
||||||
|
cartitem -> pet [label="N:1 opt", style=dashed, color="#00838F"]
|
||||||
|
cartpetreason -> pet [label="N:1", color="#00838F"]
|
||||||
|
|
||||||
|
// ServiceRequest relationships
|
||||||
|
servicerequest -> cart [label="1:1", color="#C62828"]
|
||||||
|
servicerequest -> veterinarian [label="N:1 opt\n(assigned)", style=dashed, color="#C62828"]
|
||||||
|
servicerequest -> statehistory [label="1:N audit", color="#C62828"]
|
||||||
|
servicerequest -> vetasked [label="1:N", color="#C62828"]
|
||||||
|
servicerequest -> payreminder [label="1:N", color="#C62828"]
|
||||||
|
servicerequest -> campaign [label="N:1 opt", style=dashed, color="#666"]
|
||||||
|
servicerequest -> tag [label="N:M", color="#666"]
|
||||||
|
vetasked -> veterinarian [label="N:1", color="#C62828"]
|
||||||
|
vetasked -> vetreminder [label="1:N", color="#C62828"]
|
||||||
|
|
||||||
|
// VetVisit relationships
|
||||||
|
vetvisit -> servicerequest [label="1:1 opt\n(from request)", style=dashed, color="#F9A825"]
|
||||||
|
vetvisit -> veterinarian [label="N:1", color="#F9A825"]
|
||||||
|
vetvisit -> visit_pets [label="1:N", color="#F9A825"]
|
||||||
|
visit_pets -> pet [label="N:1", color="#F9A825"]
|
||||||
|
vetvisit -> vetvisitreport [label="1:N", color="#F9A825"]
|
||||||
|
vetvisit -> vetvisitpetreason [label="1:N", color="#F9A825"]
|
||||||
|
vetvisitreport -> pet [label="N:1", color="#F9A825"]
|
||||||
|
vetvisitreport -> vetvisitfollowup [label="1:N", color="#F9A825"]
|
||||||
|
vetvisitpetreason -> pet [label="N:1", color="#F9A825"]
|
||||||
|
|
||||||
|
// AFIP
|
||||||
|
vetvisit -> receipt [label="1:1 opt\n(invoice)", style=dashed, color="#AD1457"]
|
||||||
|
|
||||||
|
// Geography
|
||||||
|
province -> locality [label="1:N", color="#666"]
|
||||||
|
}
|
||||||
976
cfg/amar/soleprint/atlas/books/arch/03-data-model.svg
Normal file
976
cfg/amar/soleprint/atlas/books/arch/03-data-model.svg
Normal file
@@ -0,0 +1,976 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.0.5 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: DataModel Pages: 1 -->
|
||||||
|
<svg width="2547pt" height="1691pt"
|
||||||
|
viewBox="0.00 0.00 2547.00 1691.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1687.2)">
|
||||||
|
<title>DataModel</title>
|
||||||
|
<polygon fill="white" stroke="none" points="-4,4 -4,-1687.2 2543,-1687.2 2543,4 -4,4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1269.5" y="-1664" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas - Data Model (Entity Relationships)</text>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_auth</title>
|
||||||
|
<path fill="#e8f5e9" stroke="#2e7d32" d="M550,-1367.6C550,-1367.6 703,-1367.6 703,-1367.6 709,-1367.6 715,-1373.6 715,-1379.6 715,-1379.6 715,-1515 715,-1515 715,-1521 709,-1527 703,-1527 703,-1527 550,-1527 550,-1527 544,-1527 538,-1521 538,-1515 538,-1515 538,-1379.6 538,-1379.6 538,-1373.6 544,-1367.6 550,-1367.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="626.5" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Users & Authentication</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_mascotas_owners</title>
|
||||||
|
<path fill="#e3f2fd" stroke="#1565c0" d="M482,-494.2C482,-494.2 678,-494.2 678,-494.2 684,-494.2 690,-500.2 690,-506.2 690,-506.2 690,-1192.4 690,-1192.4 690,-1198.4 684,-1204.4 678,-1204.4 678,-1204.4 482,-1204.4 482,-1204.4 476,-1204.4 470,-1198.4 470,-1192.4 470,-1192.4 470,-506.2 470,-506.2 470,-500.2 476,-494.2 482,-494.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="580" y="-1185.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Pet Owners & Pets</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust3" class="cluster">
|
||||||
|
<title>cluster_veterinarians</title>
|
||||||
|
<path fill="#fff3e0" stroke="#e65100" d="M806,-317.6C806,-317.6 1867,-317.6 1867,-317.6 1873,-317.6 1879,-323.6 1879,-329.6 1879,-329.6 1879,-647.6 1879,-647.6 1879,-653.6 1873,-659.6 1867,-659.6 1867,-659.6 806,-659.6 806,-659.6 800,-659.6 794,-653.6 794,-647.6 794,-647.6 794,-329.6 794,-329.6 794,-323.6 800,-317.6 806,-317.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1336.5" y="-640.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinarians</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust4" class="cluster">
|
||||||
|
<title>cluster_productos</title>
|
||||||
|
<path fill="#f3e5f5" stroke="#7b1fa2" d="M67,-476.2C67,-476.2 318,-476.2 318,-476.2 324,-476.2 330,-482.2 330,-488.2 330,-488.2 330,-1515 330,-1515 330,-1521 324,-1527 318,-1527 318,-1527 67,-1527 67,-1527 61,-1527 55,-1521 55,-1515 55,-1515 55,-488.2 55,-488.2 55,-482.2 61,-476.2 67,-476.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="192.5" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services & Pricing</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust5" class="cluster">
|
||||||
|
<title>cluster_cart</title>
|
||||||
|
<path fill="#e0f7fa" stroke="#00838f" d="M740,-991C740,-991 1066,-991 1066,-991 1072,-991 1078,-997 1078,-1003 1078,-1003 1078,-1515 1078,-1515 1078,-1521 1072,-1527 1066,-1527 1066,-1527 740,-1527 740,-1527 734,-1527 728,-1521 728,-1515 728,-1515 728,-1003 728,-1003 728,-997 734,-991 740,-991"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="903" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Cart & Checkout</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust6" class="cluster">
|
||||||
|
<title>cluster_solicitudes</title>
|
||||||
|
<path fill="#ffebee" stroke="#c62828" d="M1899,-494.2C1899,-494.2 2297,-494.2 2297,-494.2 2303,-494.2 2309,-500.2 2309,-506.2 2309,-506.2 2309,-1216.4 2309,-1216.4 2309,-1222.4 2303,-1228.4 2297,-1228.4 2297,-1228.4 1899,-1228.4 1899,-1228.4 1893,-1228.4 1887,-1222.4 1887,-1216.4 1887,-1216.4 1887,-506.2 1887,-506.2 1887,-500.2 1893,-494.2 1899,-494.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2098" y="-1209.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Service Requests (Workflow)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust7" class="cluster">
|
||||||
|
<title>cluster_vetvisits</title>
|
||||||
|
<path fill="#fffde7" stroke="#f9a825" d="M1186,-762.8C1186,-762.8 1726,-762.8 1726,-762.8 1732,-762.8 1738,-768.8 1738,-774.8 1738,-774.8 1738,-1617 1738,-1617 1738,-1623 1732,-1629 1726,-1629 1726,-1629 1186,-1629 1186,-1629 1180,-1629 1174,-1623 1174,-1617 1174,-1617 1174,-774.8 1174,-774.8 1174,-768.8 1180,-762.8 1186,-762.8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1456" y="-1609.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinary Visits</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust8" class="cluster">
|
||||||
|
<title>cluster_reference</title>
|
||||||
|
<path fill="#eceff1" stroke="#455a64" d="M20,-8C20,-8 1189,-8 1189,-8 1195,-8 1201,-14 1201,-20 1201,-20 1201,-278 1201,-278 1201,-284 1195,-290 1189,-290 1189,-290 20,-290 20,-290 14,-290 8,-284 8,-278 8,-278 8,-20 8,-20 8,-14 14,-8 20,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="604.5" y="-270.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reference Data</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust9" class="cluster">
|
||||||
|
<title>cluster_afip</title>
|
||||||
|
<path fill="#fce4ec" stroke="#ad1457" d="M2399,-997C2399,-997 2519,-997 2519,-997 2525,-997 2531,-1003 2531,-1009 2531,-1009 2531,-1180.4 2531,-1180.4 2531,-1186.4 2525,-1192.4 2519,-1192.4 2519,-1192.4 2399,-1192.4 2399,-1192.4 2393,-1192.4 2387,-1186.4 2387,-1180.4 2387,-1180.4 2387,-1009 2387,-1009 2387,-1003 2393,-997 2399,-997"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2459" y="-1173.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AFIP Invoicing</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>auth_user</title>
|
||||||
|
<polygon fill="#c8e6c9" stroke="black" points="604.14,-1376.1 604.14,-1464.1 701.86,-1464.1 701.86,-1376.1 604.14,-1376.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="653" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">auth.User</text>
|
||||||
|
<polyline fill="none" stroke="black" points="604.14,-1444.1 701.86,-1444.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">user name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">email: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_staff: bool</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_superuser: bool</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>petowner</title>
|
||||||
|
<polygon fill="#bbdefb" stroke="black" points="565.81,-993.5 565.81,-1141.5 680.19,-1141.5 680.19,-993.5 565.81,-993.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="623" y="-1128.5" font-family="Helvetica,sans-Serif" font-size="10.00">PetOwner</text>
|
||||||
|
<polyline fill="none" stroke="black" points="565.81,-1121.5 680.19,-1121.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1108.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">email: str (unique)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">phone: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">first_name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">last_name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">neighborhood_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">campaign_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">geo_latitude: float</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">geo_longitude: float</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1000.5" font-family="Helvetica,sans-Serif" font-size="10.00">address: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user->petowner -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>auth_user->petowner</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M642.17,-1375.8C642.17,-1375.8 642.17,-1153.26 642.17,-1153.26"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="645.67,-1153.26 642.17,-1143.26 638.67,-1153.26 645.67,-1153.26"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="645.23" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>veterinarian</title>
|
||||||
|
<polygon fill="#ffe0b2" stroke="black" points="1538.04,-508.7 1538.04,-596.7 1607.96,-596.7 1607.96,-508.7 1538.04,-508.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1573" y="-583.7" font-family="Helvetica,sans-Serif" font-size="10.00">Veterinarian</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1538.04,-576.7 1607.96,-576.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">user_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">license: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">email: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">phone: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user->veterinarian -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>auth_user->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#2e7d32" d="M695.21,-1375.77C695.21,-1200.06 695.21,-562 695.21,-562 695.21,-562 1526.26,-562 1526.26,-562"/>
|
||||||
|
<polygon fill="#2e7d32" stroke="#2e7d32" points="1526.26,-565.5 1536.26,-562 1526.26,-558.5 1526.26,-565.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="787.56" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>pet</title>
|
||||||
|
<polygon fill="#bbdefb" stroke="black" points="574.97,-705.3 574.97,-913.3 681.03,-913.3 681.03,-705.3 574.97,-705.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="628" y="-900.3" font-family="Helvetica,sans-Serif" font-size="10.00">Pet</text>
|
||||||
|
<polyline fill="none" stroke="black" points="574.97,-893.3 681.03,-893.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-880.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-868.3" font-family="Helvetica,sans-Serif" font-size="10.00">owner_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-856.3" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-844.3" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-832.3" font-family="Helvetica,sans-Serif" font-size="10.00">breed_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">gender: M/F</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">age: int</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">weight: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">height: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">birth_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-760.3" font-family="Helvetica,sans-Serif" font-size="10.00">allergies: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-748.3" font-family="Helvetica,sans-Serif" font-size="10.00">neutered: bool</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-736.3" font-family="Helvetica,sans-Serif" font-size="10.00">is_deceased: bool</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-724.3" font-family="Helvetica,sans-Serif" font-size="10.00">state: puppy/adult/...</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="582.97" y="-712.3" font-family="Helvetica,sans-Serif" font-size="10.00">profile_picture: file</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->pet -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>petowner->pet</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M627.58,-993.16C627.58,-993.16 627.58,-924.87 627.58,-924.87"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="631.08,-924.87 627.58,-914.87 624.08,-924.87 631.08,-924.87"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="588.67" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N owns</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart -->
|
||||||
|
<g id="node17" class="node">
|
||||||
|
<title>cart</title>
|
||||||
|
<polygon fill="#b2ebf2" stroke="black" points="906.36,-1376.1 906.36,-1464.1 1029.64,-1464.1 1029.64,-1376.1 906.36,-1376.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="968" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">Cart</text>
|
||||||
|
<polyline fill="none" stroke="black" points="906.36,-1444.1 1029.64,-1444.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">petowner_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">use_vet_prices: bool</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">apply_turn_fee: bool</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->cart -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>petowner->cart</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M680.65,-1130C765.7,-1130 914.82,-1130 914.82,-1130 914.82,-1130 914.82,-1364.21 914.82,-1364.21"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="911.32,-1364.21 914.82,-1374.21 918.32,-1364.21 911.32,-1364.21"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="790.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest -->
|
||||||
|
<g id="node21" class="node">
|
||||||
|
<title>servicerequest</title>
|
||||||
|
<polygon fill="#ffcdd2" stroke="black" points="1895.4,-969.5 1895.4,-1165.5 2032.6,-1165.5 2032.6,-969.5 1895.4,-969.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1964" y="-1152.5" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceRequest</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1895.4,-1145.5 2032.6,-1145.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1132.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1120.5" font-family="Helvetica,sans-Serif" font-size="10.00">petowner_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1108.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">state: pending/vet_asked/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00"> vet_accepted/coordinated/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00"> payed/Confirmado/...</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">days_requested: JSON</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">date_coordinated: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">hour_coordinated: time</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1000.5" font-family="Helvetica,sans-Serif" font-size="10.00">pay_number: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-988.5" font-family="Helvetica,sans-Serif" font-size="10.00">campaign_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-976.5" font-family="Helvetica,sans-Serif" font-size="10.00">attended_by_id: FK (opt)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->servicerequest -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>petowner->servicerequest</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M623.15,-1141.95C623.15,-1145.73 623.15,-1148 623.15,-1148 623.15,-1148 1883.69,-1148 1883.69,-1148"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1883.69,-1151.5 1893.69,-1148 1883.69,-1144.5 1883.69,-1151.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1109" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N requests</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit -->
|
||||||
|
<g id="node26" class="node">
|
||||||
|
<title>vetvisit</title>
|
||||||
|
<polygon fill="#fff9c4" stroke="black" points="1588.19,-1274.1 1588.19,-1566.1 1729.81,-1566.1 1729.81,-1274.1 1588.19,-1274.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1659" y="-1553.1" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisit</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1588.19,-1546.1 1729.81,-1546.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1533.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1521.1" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1509.1" font-family="Helvetica,sans-Serif" font-size="10.00">owner_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1497.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1485.1" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1473.1" font-family="Helvetica,sans-Serif" font-size="10.00">hour: time</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1461.1" font-family="Helvetica,sans-Serif" font-size="10.00">visit_type: clinical/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1449.1" font-family="Helvetica,sans-Serif" font-size="10.00"> vaccination/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1437.1" font-family="Helvetica,sans-Serif" font-size="10.00"> telemedicina</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1425.1" font-family="Helvetica,sans-Serif" font-size="10.00">visit_state: PENDING/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1413.1" font-family="Helvetica,sans-Serif" font-size="10.00"> IN_PROGRESS/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1401.1" font-family="Helvetica,sans-Serif" font-size="10.00"> COMPLETED/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1389.1" font-family="Helvetica,sans-Serif" font-size="10.00"> NO_REPORT/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1377.1" font-family="Helvetica,sans-Serif" font-size="10.00"> CANCELLED</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1365.1" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1353.1" font-family="Helvetica,sans-Serif" font-size="10.00">observations: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1341.1" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1329.1" font-family="Helvetica,sans-Serif" font-size="10.00">deposit: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1317.1" font-family="Helvetica,sans-Serif" font-size="10.00">vet_fee: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1305.1" font-family="Helvetica,sans-Serif" font-size="10.00">pay_transaction: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1293.1" font-family="Helvetica,sans-Serif" font-size="10.00">google_event_id: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1281.1" font-family="Helvetica,sans-Serif" font-size="10.00">afip_receipt_id: FK (opt)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->vetvisit -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>petowner->vetvisit</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M661.18,-1141.78C661.18,-1223.08 661.18,-1342 661.18,-1342 661.18,-1342 1576.53,-1342 1576.53,-1342"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1576.53,-1345.5 1586.53,-1342 1576.53,-1338.5 1576.53,-1345.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1249.57" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N as owner</text>
|
||||||
|
</g>
|
||||||
|
<!-- neighborhood -->
|
||||||
|
<g id="node32" class="node">
|
||||||
|
<title>neighborhood</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="139.79,-139.1 139.79,-215.1 284.21,-215.1 284.21,-139.1 139.79,-139.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="212" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Neighborhood</text>
|
||||||
|
<polyline fill="none" stroke="black" points="139.79,-195.1 284.21,-195.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="147.79" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="147.79" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="147.79" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">distance_coefficient: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="147.79" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">coverage_area: GIS Polygon</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->neighborhood -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>petowner->neighborhood</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M565.5,-1008C489.96,-1008 366.79,-1008 366.79,-1008 366.79,-1008 366.79,-210 366.79,-210 366.79,-210 295.92,-210 295.92,-210"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="295.92,-206.5 285.92,-210 295.92,-213.5 295.92,-206.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="25.56" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 lives in</text>
|
||||||
|
</g>
|
||||||
|
<!-- campaign -->
|
||||||
|
<g id="node38" class="node">
|
||||||
|
<title>campaign</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="828.77,-133.1 828.77,-221.1 919.23,-221.1 919.23,-133.1 828.77,-133.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="874" y="-208.1" font-family="Helvetica,sans-Serif" font-size="10.00">Campaign</text>
|
||||||
|
<polyline fill="none" stroke="black" points="828.77,-201.1 919.23,-201.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="836.77" y="-188.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="836.77" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="836.77" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">utm_source: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="836.77" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">utm_medium: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="836.77" y="-140.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_active: bool</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->campaign -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>petowner->campaign</title>
|
||||||
|
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M680.53,-1005C685.43,-1005 688.57,-1005 688.57,-1005 688.57,-1005 688.57,-210 688.57,-210 688.57,-210 817.13,-210 817.13,-210"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="817.13,-213.5 827.13,-210 817.13,-206.5 817.13,-213.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="408.9" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- petvaccine -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>petvaccine</title>
|
||||||
|
<polygon fill="#90caf9" stroke="black" points="568.07,-508.7 568.07,-596.7 681.93,-596.7 681.93,-508.7 568.07,-508.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="625" y="-583.7" font-family="Helvetica,sans-Serif" font-size="10.00">PetVaccine</text>
|
||||||
|
<polyline fill="none" stroke="black" points="568.07,-576.7 681.93,-576.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="576.07" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="576.07" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="576.07" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">vaccine_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="576.07" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">application_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="576.07" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">next_application: date</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->petvaccine -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>pet->petvaccine</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M628,-704.93C628,-704.93 628,-608.65 628,-608.65"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="631.5,-608.65 628,-598.65 624.5,-608.65 631.5,-608.65"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="633.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- petstudy -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>petstudy</title>
|
||||||
|
<polygon fill="#90caf9" stroke="black" points="478.21,-502.7 478.21,-602.7 549.79,-602.7 549.79,-502.7 478.21,-502.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="514" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">PetStudy</text>
|
||||||
|
<polyline fill="none" stroke="black" points="478.21,-582.7 549.79,-582.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="486.21" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="486.21" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="486.21" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">study_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="486.21" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="486.21" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">result: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="486.21" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">images: files</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->petstudy -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>pet->petstudy</title>
|
||||||
|
<path fill="none" stroke="#1565c0" d="M574.67,-713C545,-713 514,-713 514,-713 514,-713 514,-614.49 514,-614.49"/>
|
||||||
|
<polygon fill="#1565c0" stroke="#1565c0" points="517.5,-614.49 514,-604.49 510.5,-614.49 517.5,-614.49"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="558.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- petbreed -->
|
||||||
|
<g id="node35" class="node">
|
||||||
|
<title>petbreed</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="16.26,-145.1 16.26,-209.1 121.74,-209.1 121.74,-145.1 16.26,-145.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="69" y="-196.1" font-family="Helvetica,sans-Serif" font-size="10.00">PetBreed</text>
|
||||||
|
<polyline fill="none" stroke="black" points="16.26,-189.1 121.74,-189.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="24.26" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="24.26" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="24.26" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->petbreed -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>pet->petbreed</title>
|
||||||
|
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M574.5,-721C429.5,-721 39.78,-721 39.78,-721 39.78,-721 39.78,-221.01 39.78,-221.01"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="43.28,-221.01 39.78,-211.01 36.28,-221.01 43.28,-221.01"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="457.9" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- vaccine -->
|
||||||
|
<g id="node36" class="node">
|
||||||
|
<title>vaccine</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="501.32,-139.1 501.32,-215.1 620.68,-215.1 620.68,-139.1 501.32,-139.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="561" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Vaccine</text>
|
||||||
|
<polyline fill="none" stroke="black" points="501.32,-195.1 620.68,-195.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="509.32" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="509.32" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="509.32" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="509.32" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">periodicity: int (months)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petvaccine->vaccine -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>petvaccine->vaccine</title>
|
||||||
|
<path fill="none" stroke="#666666" d="M594.38,-508.51C594.38,-508.51 594.38,-226.98 594.38,-226.98"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="597.88,-226.98 594.38,-216.98 590.88,-226.98 597.88,-226.98"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="581.22" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- study -->
|
||||||
|
<g id="node37" class="node">
|
||||||
|
<title>study</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="378.26,-139.1 378.26,-215.1 483.74,-215.1 483.74,-139.1 378.26,-139.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="431" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Study</text>
|
||||||
|
<polyline fill="none" stroke="black" points="378.26,-195.1 483.74,-195.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="386.26" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="386.26" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="386.26" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="386.26" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">group_id: FK (opt)</text>
|
||||||
|
</g>
|
||||||
|
<!-- petstudy->study -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>petstudy->study</title>
|
||||||
|
<path fill="none" stroke="#666666" d="M492.53,-502.42C492.53,-400.05 492.53,-177 492.53,-177 492.53,-177 491.7,-177 491.7,-177"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="495.74,-173.5 485.74,-177 495.74,-180.5 495.74,-173.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="495.22" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- availability -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>availability</title>
|
||||||
|
<polygon fill="#ffcc80" stroke="black" points="1653.03,-332.1 1653.03,-432.1 1752.97,-432.1 1752.97,-332.1 1653.03,-332.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1703" y="-419.1" font-family="Helvetica,sans-Serif" font-size="10.00">Availability</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1653.03,-412.1 1752.97,-412.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-399.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-387.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-375.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_day: 0-6</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-363.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_day: 0-6</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-351.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_time: time</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-339.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_time: time</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->availability -->
|
||||||
|
<g id="edge18" class="edge">
|
||||||
|
<title>veterinarian->availability</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M1608.25,-530C1642.2,-530 1688.41,-530 1688.41,-530 1688.41,-530 1688.41,-444.1 1688.41,-444.1"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1691.91,-444.1 1688.41,-434.1 1684.91,-444.1 1691.91,-444.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1653.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- unavailability -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>unavailability</title>
|
||||||
|
<polygon fill="#ffcc80" stroke="black" points="1771.03,-326.1 1771.03,-438.1 1870.97,-438.1 1870.97,-326.1 1771.03,-326.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1821" y="-425.1" font-family="Helvetica,sans-Serif" font-size="10.00">Unavailability</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1771.03,-418.1 1870.97,-418.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-405.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-393.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-381.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-369.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-357.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_time: time</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-345.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_time: time</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-333.1" font-family="Helvetica,sans-Serif" font-size="10.00">reason: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->unavailability -->
|
||||||
|
<g id="edge19" class="edge">
|
||||||
|
<title>veterinarian->unavailability</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M1608.27,-553C1676.74,-553 1821,-553 1821,-553 1821,-553 1821,-450.05 1821,-450.05"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1824.5,-450.05 1821,-440.05 1817.5,-450.05 1824.5,-450.05"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1729.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_specialty -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>vet_specialty</title>
|
||||||
|
<polygon fill="#ffb74d" stroke="black" points="1442,-426.1 1249.44,-382.1 1442,-338.1 1634.56,-382.1 1442,-426.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1283.24" y="-391.1" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Vet-Specialty|veterinarian_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1283.24" y="-379.1" font-family="Helvetica,sans-Serif" font-size="10.00">specialty_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1442" y="-367.1" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->vet_specialty -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>veterinarian->vet_specialty</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M1584.65,-508.32C1584.65,-508.32 1584.65,-405.5 1584.65,-405.5"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1588.15,-405.5 1584.65,-395.5 1581.15,-405.5 1588.15,-405.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1505.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_neighborhood -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>vet_neighborhood</title>
|
||||||
|
<polygon fill="#ffb74d" stroke="black" points="1017,-426.1 802.19,-382.1 1017,-338.1 1231.81,-382.1 1017,-426.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="838.97" y="-391.1" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Vet-Neighborhood|veterinarian_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="838.97" y="-379.1" font-family="Helvetica,sans-Serif" font-size="10.00">neighborhood_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1017" y="-367.1" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->vet_neighborhood -->
|
||||||
|
<g id="edge16" class="edge">
|
||||||
|
<title>veterinarian->vet_neighborhood</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M1537.71,-544C1447.43,-544 1215.15,-544 1215.15,-544 1215.15,-544 1215.15,-397.3 1215.15,-397.3"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1218.65,-397.3 1215.15,-387.3 1211.65,-397.3 1218.65,-397.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1273.79" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N coverage</text>
|
||||||
|
</g>
|
||||||
|
<!-- turnfeegroup -->
|
||||||
|
<g id="node41" class="node">
|
||||||
|
<title>turnfeegroup</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="1067.24,-145.1 1067.24,-209.1 1192.76,-209.1 1192.76,-145.1 1067.24,-145.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1130" y="-196.1" font-family="Helvetica,sans-Serif" font-size="10.00">IndividualTurnFeeGroup</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1067.24,-189.1 1192.76,-189.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">fee_percentage: decimal</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->turnfeegroup -->
|
||||||
|
<g id="edge20" class="edge">
|
||||||
|
<title>veterinarian->turnfeegroup</title>
|
||||||
|
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1537.74,-526C1452.2,-526 1240.63,-526 1240.63,-526 1240.63,-526 1240.63,-177 1240.63,-177 1240.63,-177 1204.58,-177 1204.58,-177"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="1204.58,-173.5 1194.58,-177 1204.58,-180.5 1204.58,-173.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="777.33" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:M</text>
|
||||||
|
</g>
|
||||||
|
<!-- specialty -->
|
||||||
|
<g id="node31" class="node">
|
||||||
|
<title>specialty</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="753.16,-151.1 753.16,-203.1 810.84,-203.1 810.84,-151.1 753.16,-151.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="782" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Specialty</text>
|
||||||
|
<polyline fill="none" stroke="black" points="753.16,-183.1 810.84,-183.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="761.16" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="761.16" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_specialty->specialty -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>vet_specialty->specialty</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M1442,-337.81C1442,-315.45 1442,-293 1442,-293 1442,-293 806.51,-293 806.51,-293 806.51,-293 806.51,-214.91 806.51,-214.91"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="810.01,-214.91 806.51,-204.91 803.01,-214.91 810.01,-214.91"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="751.22" y="-300.4" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_neighborhood->neighborhood -->
|
||||||
|
<g id="edge17" class="edge">
|
||||||
|
<title>vet_neighborhood->neighborhood</title>
|
||||||
|
<path fill="none" stroke="#e65100" d="M800.26,-382C572.59,-382 247.21,-382 247.21,-382 247.21,-382 247.21,-226.67 247.21,-226.67"/>
|
||||||
|
<polygon fill="#e65100" stroke="#e65100" points="250.71,-226.67 247.21,-216.67 243.71,-226.67 250.71,-226.67"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="657.22" y="-300.4" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- grupo -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>grupo</title>
|
||||||
|
<polygon fill="#e1bee7" stroke="black" points="85.98,-1388.1 85.98,-1452.1 172.02,-1452.1 172.02,-1388.1 85.98,-1388.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="129" y="-1439.1" font-family="Helvetica,sans-Serif" font-size="10.00">Group</text>
|
||||||
|
<polyline fill="none" stroke="black" points="85.98,-1432.1 172.02,-1432.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
|
||||||
|
</g>
|
||||||
|
<!-- category -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>category</title>
|
||||||
|
<polygon fill="#e1bee7" stroke="black" points="166.6,-1023.5 166.6,-1111.5 255.4,-1111.5 255.4,-1023.5 166.6,-1023.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="211" y="-1098.5" font-family="Helvetica,sans-Serif" font-size="10.00">Category</text>
|
||||||
|
<polyline fill="none" stroke="black" points="166.6,-1091.5 255.4,-1091.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00">group_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00">value: int (order)</text>
|
||||||
|
</g>
|
||||||
|
<!-- grupo->category -->
|
||||||
|
<g id="edge21" class="edge">
|
||||||
|
<title>grupo->category</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M172.38,-1420C177.51,-1420 180.96,-1420 180.96,-1420 180.96,-1420 180.96,-1123.3 180.96,-1123.3"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="184.46,-1123.3 180.96,-1113.3 177.46,-1123.3 184.46,-1123.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="151.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- service -->
|
||||||
|
<g id="node13" class="node">
|
||||||
|
<title>service</title>
|
||||||
|
<polygon fill="#ce93d8" stroke="black" points="199.63,-729.3 199.63,-889.3 322.37,-889.3 322.37,-729.3 199.63,-729.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="261" y="-876.3" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
|
||||||
|
<polyline fill="none" stroke="black" points="199.63,-869.3 322.37,-869.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-856.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-844.3" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-832.3" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">specialty_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">category_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">duration: int (min)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">modality: onsite/online</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">payment_sign_req: bool</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-760.3" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type_filter: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-748.3" font-family="Helvetica,sans-Serif" font-size="10.00">age_filter: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="207.63" y="-736.3" font-family="Helvetica,sans-Serif" font-size="10.00">weight_range: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- category->service -->
|
||||||
|
<g id="edge22" class="edge">
|
||||||
|
<title>category->service</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M227.52,-1023.11C227.52,-1023.11 227.52,-901.07 227.52,-901.07"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="231.02,-901.07 227.52,-891.07 224.02,-901.08 231.02,-901.07"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="250.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- prices -->
|
||||||
|
<g id="node14" class="node">
|
||||||
|
<title>prices</title>
|
||||||
|
<polygon fill="#ba68c8" stroke="black" points="63.3,-484.7 63.3,-620.7 192.7,-620.7 192.7,-484.7 63.3,-484.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="128" y="-607.7" font-family="Helvetica,sans-Serif" font-size="10.00">Prices</text>
|
||||||
|
<polyline fill="none" stroke="black" points="63.3,-600.7 192.7,-600.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-587.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-575.7" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">professional_fee: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">payment_sign: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">from_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-503.7" font-family="Helvetica,sans-Serif" font-size="10.00">to_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="71.3" y="-491.7" font-family="Helvetica,sans-Serif" font-size="10.00">active: bool</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->prices -->
|
||||||
|
<g id="edge24" class="edge">
|
||||||
|
<title>service->prices</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M204.92,-728.98C204.92,-673.26 204.92,-609 204.92,-609 204.92,-609 203.72,-609 203.72,-609"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="204.41,-605.5 194.41,-609 204.41,-612.5 204.41,-605.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="184.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- discounts -->
|
||||||
|
<g id="node15" class="node">
|
||||||
|
<title>discounts</title>
|
||||||
|
<polygon fill="#ba68c8" stroke="black" points="210.2,-502.7 210.2,-602.7 321.8,-602.7 321.8,-502.7 210.2,-502.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="266" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">Discounts</text>
|
||||||
|
<polyline fill="none" stroke="black" points="210.2,-582.7 321.8,-582.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="218.2" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="218.2" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="218.2" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">discount: decimal (%)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="218.2" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">from_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="218.2" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">to_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="218.2" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">active: bool</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->discounts -->
|
||||||
|
<g id="edge25" class="edge">
|
||||||
|
<title>service->discounts</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" d="M247.4,-728.81C247.4,-728.81 247.4,-614.57 247.4,-614.57"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="250.9,-614.57 247.4,-604.57 243.9,-614.57 250.9,-614.57"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="270.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->specialty -->
|
||||||
|
<g id="edge23" class="edge">
|
||||||
|
<title>service->specialty</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M284.6,-728.87C284.6,-675.42 284.6,-615 284.6,-615 284.6,-615 777.67,-615 777.67,-615 777.67,-615 777.67,-215.02 777.67,-215.02"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="781.17,-215.02 777.67,-205.02 774.17,-215.02 781.17,-215.02"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="538.9" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- prices->veterinarian -->
|
||||||
|
<g id="edge26" class="edge">
|
||||||
|
<title>prices->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M192.84,-493C472.5,-493 1561.35,-493 1561.35,-493 1561.35,-493 1561.35,-496.86 1561.35,-496.86"/>
|
||||||
|
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="1557.85,-496.86 1561.35,-506.86 1564.85,-496.86 1557.85,-496.86"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="434" y="-679.6" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="434" y="-670" font-family="Helvetica,sans-Serif" font-size="8.00">(vet-specific)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicecombo -->
|
||||||
|
<g id="node16" class="node">
|
||||||
|
<title>servicecombo</title>
|
||||||
|
<polygon fill="#e1bee7" stroke="black" points="189.91,-1376.1 189.91,-1464.1 322.09,-1464.1 322.09,-1376.1 189.91,-1376.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="256" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceCombo</text>
|
||||||
|
<polyline fill="none" stroke="black" points="189.91,-1444.1 322.09,-1444.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">discount_percent: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">discount_fixed: decimal</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->veterinarian -->
|
||||||
|
<g id="edge27" class="edge">
|
||||||
|
<title>cart->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M949.06,-1375.77C949.06,-1202.29 949.06,-579 949.06,-579 949.06,-579 1526.15,-579 1526.15,-579"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="1526.15,-582.5 1536.15,-579 1526.15,-575.5 1526.15,-582.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1789.12" y="-943.8" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1789.12" y="-934.2" font-family="Helvetica,sans-Serif" font-size="8.00">(assigned vet)</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartitem -->
|
||||||
|
<g id="node18" class="node">
|
||||||
|
<title>cartitem</title>
|
||||||
|
<polygon fill="#80deea" stroke="black" points="735.93,-1011.5 735.93,-1123.5 838.07,-1123.5 838.07,-1011.5 735.93,-1011.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="787" y="-1110.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartItem</text>
|
||||||
|
<polyline fill="none" stroke="black" points="735.93,-1103.5 838.07,-1103.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1090.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK (opt)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00">quantity: int</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1018.5" font-family="Helvetica,sans-Serif" font-size="10.00">total: decimal (calc)</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->cartitem -->
|
||||||
|
<g id="edge28" class="edge">
|
||||||
|
<title>cart->cartitem</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M923.27,-1375.74C923.27,-1291.12 923.27,-1115 923.27,-1115 923.27,-1115 850.03,-1115 850.03,-1115"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="850.03,-1111.5 840.03,-1115 850.03,-1118.5 850.03,-1111.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="862.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartresumeitem -->
|
||||||
|
<g id="node19" class="node">
|
||||||
|
<title>cartresumeitem</title>
|
||||||
|
<polygon fill="#80deea" stroke="black" points="957.93,-999.5 957.93,-1135.5 1070.07,-1135.5 1070.07,-999.5 957.93,-999.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1014" y="-1122.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartResumeItem</text>
|
||||||
|
<polyline fill="none" stroke="black" points="957.93,-1115.5 1070.07,-1115.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1102.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1090.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">concept: SUBTOTAL/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00"> DESCUENTO/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00"> ADELANTO/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00"> TOTAL/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00"> COSTO_SERVICIO</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1018.5" font-family="Helvetica,sans-Serif" font-size="10.00">amount: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1006.5" font-family="Helvetica,sans-Serif" font-size="10.00">order: int</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->cartresumeitem -->
|
||||||
|
<g id="edge29" class="edge">
|
||||||
|
<title>cart->cartresumeitem</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M981.83,-1375.8C981.83,-1375.8 981.83,-1147.31 981.83,-1147.31"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="985.33,-1147.31 981.83,-1137.31 978.33,-1147.31 985.33,-1147.31"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="990.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartpetreason -->
|
||||||
|
<g id="node20" class="node">
|
||||||
|
<title>cartpetreason</title>
|
||||||
|
<polygon fill="#80deea" stroke="black" points="855.82,-1029.5 855.82,-1105.5 940.18,-1105.5 940.18,-1029.5 855.82,-1029.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="898" y="-1092.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartPetReason</text>
|
||||||
|
<polyline fill="none" stroke="black" points="855.82,-1085.5 940.18,-1085.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->cartpetreason -->
|
||||||
|
<g id="edge30" class="edge">
|
||||||
|
<title>cart->cartpetreason</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M931.73,-1375.8C931.73,-1375.8 931.73,-1117.47 931.73,-1117.47"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="935.23,-1117.47 931.73,-1107.47 928.23,-1117.47 935.23,-1117.47"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="919.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartitem->pet -->
|
||||||
|
<g id="edge32" class="edge">
|
||||||
|
<title>cartitem->pet</title>
|
||||||
|
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M747.42,-1011.12C747.42,-964.19 747.42,-905 747.42,-905 747.42,-905 692.94,-905 692.94,-905"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="692.94,-901.5 682.94,-905 692.94,-908.5 692.94,-901.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="640.9" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartitem->service -->
|
||||||
|
<g id="edge31" class="edge">
|
||||||
|
<title>cartitem->service</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M741.67,-1011.15C741.67,-997.62 741.67,-987 741.67,-987 741.67,-987 288.75,-987 288.75,-987 288.75,-987 288.75,-901.09 288.75,-901.09"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="292.25,-901.09 288.75,-891.09 285.25,-901.09 292.25,-901.09"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="446.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartpetreason->pet -->
|
||||||
|
<g id="edge33" class="edge">
|
||||||
|
<title>cartpetreason->pet</title>
|
||||||
|
<path fill="none" stroke="#00838f" d="M898,-1029.01C898,-978.45 898,-896 898,-896 898,-896 692.86,-896 692.86,-896"/>
|
||||||
|
<polygon fill="#00838f" stroke="#00838f" points="692.86,-892.5 682.86,-896 692.86,-899.5 692.86,-892.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="683.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->veterinarian -->
|
||||||
|
<g id="edge35" class="edge">
|
||||||
|
<title>servicerequest->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#c62828" stroke-dasharray="5,2" d="M1895.22,-975C1780.2,-975 1561.26,-975 1561.26,-975 1561.26,-975 1561.26,-608.61 1561.26,-608.61"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1564.76,-608.61 1561.26,-598.61 1557.76,-608.61 1564.76,-608.61"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1858.68" y="-811.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1858.68" y="-802.1" font-family="Helvetica,sans-Serif" font-size="8.00">(assigned)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->cart -->
|
||||||
|
<g id="edge34" class="edge">
|
||||||
|
<title>servicerequest->cart</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1895.08,-1154C1675.36,-1154 1005.73,-1154 1005.73,-1154 1005.73,-1154 1005.73,-1364.35 1005.73,-1364.35"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1002.23,-1364.35 1005.73,-1374.35 1009.23,-1364.35 1002.23,-1364.35"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1160.56" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- statehistory -->
|
||||||
|
<g id="node22" class="node">
|
||||||
|
<title>statehistory</title>
|
||||||
|
<polygon fill="#ef9a9a" stroke="black" points="2182.86,-759.3 2182.86,-859.3 2301.14,-859.3 2301.14,-759.3 2182.86,-759.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2242" y="-846.3" font-family="Helvetica,sans-Serif" font-size="10.00">StateHistory</text>
|
||||||
|
<polyline fill="none" stroke="black" points="2182.86,-839.3 2301.14,-839.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-826.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">state: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">additional_data: JSON</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">created_at: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-766.3" font-family="Helvetica,sans-Serif" font-size="10.00">user_id: FK</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->statehistory -->
|
||||||
|
<g id="edge36" class="edge">
|
||||||
|
<title>servicerequest->statehistory</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M2033.08,-987C2115.41,-987 2242,-987 2242,-987 2242,-987 2242,-871.26 2242,-871.26"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="2245.5,-871.26 2242,-861.26 2238.5,-871.26 2245.5,-871.26"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2179.01" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N audit</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked -->
|
||||||
|
<g id="node23" class="node">
|
||||||
|
<title>vetasked</title>
|
||||||
|
<polygon fill="#ef9a9a" stroke="black" points="1894.85,-759.3 1894.85,-859.3 2023.15,-859.3 2023.15,-759.3 1894.85,-759.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1959" y="-846.3" font-family="Helvetica,sans-Serif" font-size="10.00">VeterinarianAsked</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1894.85,-839.3 2023.15,-839.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-826.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">date_asked: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">date_answered: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-766.3" font-family="Helvetica,sans-Serif" font-size="10.00">accepted: bool</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->vetasked -->
|
||||||
|
<g id="edge37" class="edge">
|
||||||
|
<title>servicerequest->vetasked</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1959.27,-969.15C1959.27,-969.15 1959.27,-871.22 1959.27,-871.22"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1962.77,-871.22 1959.27,-861.22 1955.77,-871.22 1962.77,-871.22"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1967.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- payreminder -->
|
||||||
|
<g id="node25" class="node">
|
||||||
|
<title>payreminder</title>
|
||||||
|
<polygon fill="#e57373" stroke="black" points="2041.63,-765.3 2041.63,-853.3 2164.37,-853.3 2164.37,-765.3 2041.63,-765.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2103" y="-840.3" font-family="Helvetica,sans-Serif" font-size="10.00">PaymentReminder</text>
|
||||||
|
<polyline fill="none" stroke="black" points="2041.63,-833.3 2164.37,-833.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">scheduled_for: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">processed_at: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">status: pending/sent/...</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->payreminder -->
|
||||||
|
<g id="edge38" class="edge">
|
||||||
|
<title>servicerequest->payreminder</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M2029.45,-969.03C2029.45,-895.98 2029.45,-809 2029.45,-809 2029.45,-809 2030.63,-809 2030.63,-809"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="2029.75,-812.5 2039.75,-809 2029.75,-805.5 2029.75,-812.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2055.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->campaign -->
|
||||||
|
<g id="edge39" class="edge">
|
||||||
|
<title>servicerequest->campaign</title>
|
||||||
|
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M2026.3,-969.29C2026.3,-754.09 2026.3,-260 2026.3,-260 2026.3,-260 874,-260 874,-260 874,-260 874,-232.98 874,-232.98"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="877.5,-232.98 874,-222.98 870.5,-232.98 877.5,-232.98"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2341.9" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- tag -->
|
||||||
|
<g id="node39" class="node">
|
||||||
|
<title>tag</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="638.43,-151.1 638.43,-203.1 735.57,-203.1 735.57,-151.1 638.43,-151.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="687" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Tag</text>
|
||||||
|
<polyline fill="none" stroke="black" points="638.43,-183.1 735.57,-183.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="646.43" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="646.43" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str (unique)</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->tag -->
|
||||||
|
<g id="edge40" class="edge">
|
||||||
|
<title>servicerequest->tag</title>
|
||||||
|
<path fill="none" stroke="#666666" d="M1895.27,-981C1634.21,-981 718.72,-981 718.72,-981 718.72,-981 718.72,-214.9 718.72,-214.9"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="722.22,-214.9 718.72,-204.9 715.22,-214.9 722.22,-214.9"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="756.33" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:M</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked->veterinarian -->
|
||||||
|
<g id="edge41" class="edge">
|
||||||
|
<title>vetasked->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1894.62,-765C1787.53,-765 1584.47,-765 1584.47,-765 1584.47,-765 1584.47,-608.46 1584.47,-608.46"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1587.97,-608.46 1584.47,-598.46 1580.97,-608.46 1587.97,-608.46"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1848.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetreminder -->
|
||||||
|
<g id="node24" class="node">
|
||||||
|
<title>vetreminder</title>
|
||||||
|
<polygon fill="#e57373" stroke="black" points="1897.63,-502.7 1897.63,-602.7 2020.37,-602.7 2020.37,-502.7 1897.63,-502.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1959" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">ScheduledVetReminder</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1897.63,-582.7 2020.37,-582.7"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">vet_asked_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">scheduled_for: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">processed_at: datetime</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">status: pending/sent/...</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">celery_task_id: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked->vetreminder -->
|
||||||
|
<g id="edge42" class="edge">
|
||||||
|
<title>vetasked->vetreminder</title>
|
||||||
|
<path fill="none" stroke="#c62828" d="M1959,-758.91C1959,-758.91 1959,-614.49 1959,-614.49"/>
|
||||||
|
<polygon fill="#c62828" stroke="#c62828" points="1962.5,-614.49 1959,-604.49 1955.5,-614.49 1962.5,-614.49"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1965.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->veterinarian -->
|
||||||
|
<g id="edge44" class="edge">
|
||||||
|
<title>vetvisit->veterinarian</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1617.03,-1273.86C1617.03,-1031.98 1617.03,-575 1617.03,-575 1617.03,-575 1616.13,-575 1616.13,-575"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1619.63,-571.5 1609.63,-575 1619.63,-578.5 1619.63,-571.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2373.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->servicerequest -->
|
||||||
|
<g id="edge43" class="edge">
|
||||||
|
<title>vetvisit->servicerequest</title>
|
||||||
|
<path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M1695.23,-1273.71C1695.23,-1214.56 1695.23,-1160 1695.23,-1160 1695.23,-1160 1883.62,-1160 1883.62,-1160"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1883.62,-1163.5 1893.62,-1160 1883.62,-1156.5 1883.62,-1163.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1864.12" y="-1248.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1864.12" y="-1238.8" font-family="Helvetica,sans-Serif" font-size="8.00">(from request)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport -->
|
||||||
|
<g id="node27" class="node">
|
||||||
|
<title>vetvisitreport</title>
|
||||||
|
<polygon fill="#fff59d" stroke="black" points="1626.09,-1005.5 1626.09,-1129.5 1729.91,-1129.5 1729.91,-1005.5 1626.09,-1005.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1678" y="-1116.5" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitReport</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1626.09,-1109.5 1729.91,-1109.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">visit_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">physical_exam: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">diagnosis: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">treatment: text</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">pdf_file: file</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->vetvisitreport -->
|
||||||
|
<g id="edge47" class="edge">
|
||||||
|
<title>vetvisit->vetvisitreport</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1660.66,-1273.63C1660.66,-1273.63 1660.66,-1141.19 1660.66,-1141.19"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1664.16,-1141.19 1660.66,-1131.19 1657.16,-1141.19 1664.16,-1141.19"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1675.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitpetreason -->
|
||||||
|
<g id="node29" class="node">
|
||||||
|
<title>vetvisitpetreason</title>
|
||||||
|
<polygon fill="#fff176" stroke="black" points="1508.31,-1029.5 1508.31,-1105.5 1607.69,-1105.5 1607.69,-1029.5 1508.31,-1029.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1558" y="-1092.5" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitPetReason</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1508.31,-1085.5 1607.69,-1085.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">visit_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->vetvisitpetreason -->
|
||||||
|
<g id="edge48" class="edge">
|
||||||
|
<title>vetvisit->vetvisitpetreason</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1597.94,-1273.63C1597.94,-1273.63 1597.94,-1117.18 1597.94,-1117.18"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1601.44,-1117.18 1597.94,-1107.18 1594.44,-1117.18 1601.44,-1117.18"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1598.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- visit_pets -->
|
||||||
|
<g id="node30" class="node">
|
||||||
|
<title>visit_pets</title>
|
||||||
|
<polygon fill="#ffee58" stroke="black" points="1336,-1111.5 1181.81,-1067.5 1336,-1023.5 1490.19,-1067.5 1336,-1111.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1210.47" y="-1076.5" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Visit-Pets|vetvisit_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1210.47" y="-1064.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1336" y="-1052.5" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->visit_pets -->
|
||||||
|
<g id="edge45" class="edge">
|
||||||
|
<title>vetvisit->visit_pets</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1587.72,-1308C1492.6,-1308 1336,-1308 1336,-1308 1336,-1308 1336,-1123.41 1336,-1123.41"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1339.5,-1123.41 1336,-1113.41 1332.5,-1123.41 1339.5,-1123.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1476.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- receipt -->
|
||||||
|
<g id="node42" class="node">
|
||||||
|
<title>receipt</title>
|
||||||
|
<polygon fill="#f8bbd9" stroke="black" points="2394.58,-1005.5 2394.58,-1129.5 2523.42,-1129.5 2523.42,-1005.5 2394.58,-1005.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2459" y="-1116.5" font-family="Helvetica,sans-Serif" font-size="10.00">Receipt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="2394.58,-1109.5 2523.42,-1109.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">document_number: bigint</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">receipt_number: int</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">issued_date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">total_amount: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">net_taxed: decimal</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">cae: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">cae_expiration: date</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->receipt -->
|
||||||
|
<g id="edge52" class="edge">
|
||||||
|
<title>vetvisit->receipt</title>
|
||||||
|
<path fill="none" stroke="#ad1457" stroke-dasharray="5,2" d="M1729.99,-1420C1925.78,-1420 2459,-1420 2459,-1420 2459,-1420 2459,-1141.22 2459,-1141.22"/>
|
||||||
|
<polygon fill="#ad1457" stroke="#ad1457" points="2462.5,-1141.22 2459,-1131.22 2455.5,-1141.22 2462.5,-1141.22"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2444.12" y="-1248.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2444.12" y="-1238.8" font-family="Helvetica,sans-Serif" font-size="8.00">(invoice)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport->pet -->
|
||||||
|
<g id="edge49" class="edge">
|
||||||
|
<title>vetvisitreport->pet</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1662.74,-1005.24C1662.74,-947.02 1662.74,-869 1662.74,-869 1662.74,-869 692.97,-869 692.97,-869"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="692.97,-865.5 682.97,-869 692.97,-872.5 692.97,-865.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1237.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitfollowup -->
|
||||||
|
<g id="node28" class="node">
|
||||||
|
<title>vetvisitfollowup</title>
|
||||||
|
<polygon fill="#fff176" stroke="black" points="1632.21,-771.3 1632.21,-847.3 1723.79,-847.3 1723.79,-771.3 1632.21,-771.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1678" y="-834.3" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitFollowUp</text>
|
||||||
|
<polyline fill="none" stroke="black" points="1632.21,-827.3 1723.79,-827.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">report_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport->vetvisitfollowup -->
|
||||||
|
<g id="edge50" class="edge">
|
||||||
|
<title>vetvisitreport->vetvisitfollowup</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1693.26,-1005.33C1693.26,-1005.33 1693.26,-859.22 1693.26,-859.22"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="1696.76,-859.22 1693.26,-849.22 1689.76,-859.22 1696.76,-859.22"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1684.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitpetreason->pet -->
|
||||||
|
<g id="edge51" class="edge">
|
||||||
|
<title>vetvisitpetreason->pet</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1523.18,-1029.15C1523.18,-973.78 1523.18,-878 1523.18,-878 1523.18,-878 692.83,-878 692.83,-878"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="692.83,-874.5 682.83,-878 692.83,-881.5 692.83,-874.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="753.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- visit_pets->pet -->
|
||||||
|
<g id="edge46" class="edge">
|
||||||
|
<title>visit_pets->pet</title>
|
||||||
|
<path fill="none" stroke="#f9a825" d="M1198.48,-1062.29C1198.48,-1032.7 1198.48,-887 1198.48,-887 1198.48,-887 692.93,-887 692.93,-887"/>
|
||||||
|
<polygon fill="#f9a825" stroke="#f9a825" points="692.93,-883.5 682.93,-887 692.93,-890.5 692.93,-883.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="718.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- province -->
|
||||||
|
<g id="node33" class="node">
|
||||||
|
<title>province</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="302.16,-151.1 302.16,-203.1 359.84,-203.1 359.84,-151.1 302.16,-151.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="331" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Province</text>
|
||||||
|
<polyline fill="none" stroke="black" points="302.16,-183.1 359.84,-183.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="310.16" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="310.16" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- locality -->
|
||||||
|
<g id="node34" class="node">
|
||||||
|
<title>locality</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="288.26,-16.5 288.26,-80.5 373.74,-80.5 373.74,-16.5 288.26,-16.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="331" y="-67.5" font-family="Helvetica,sans-Serif" font-size="10.00">Locality</text>
|
||||||
|
<polyline fill="none" stroke="black" points="288.26,-60.5 373.74,-60.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="296.26" y="-47.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="296.26" y="-35.5" font-family="Helvetica,sans-Serif" font-size="10.00">province_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="296.26" y="-23.5" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
</g>
|
||||||
|
<!-- province->locality -->
|
||||||
|
<g id="edge53" class="edge">
|
||||||
|
<title>province->locality</title>
|
||||||
|
<path fill="none" stroke="#666666" d="M331,-150.77C331,-150.77 331,-92.24 331,-92.24"/>
|
||||||
|
<polygon fill="#666666" stroke="#666666" points="334.5,-92.24 331,-82.24 327.5,-92.24 334.5,-92.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="337.22" y="-101.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- medication -->
|
||||||
|
<g id="node40" class="node">
|
||||||
|
<title>medication</title>
|
||||||
|
<polygon fill="#cfd8dc" stroke="black" points="937.19,-127.1 937.19,-227.1 1048.81,-227.1 1048.81,-127.1 937.19,-127.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="993" y="-214.1" font-family="Helvetica,sans-Serif" font-size="10.00">Medication</text>
|
||||||
|
<polyline fill="none" stroke="black" points="937.19,-207.1 1048.81,-207.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="945.19" y="-194.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="945.19" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="945.19" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">general_name_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="945.19" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">type_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="945.19" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">presentation_id: FK</text>
|
||||||
|
<text xml:space="preserve" text-anchor="start" x="945.19" y="-134.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: str</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 84 KiB |
195
cfg/amar/soleprint/atlas/books/arch/04-data-model-simple.dot
Normal file
195
cfg/amar/soleprint/atlas/books/arch/04-data-model-simple.dot
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
digraph DataModelSimple {
|
||||||
|
rankdir=TB
|
||||||
|
compound=true
|
||||||
|
splines=ortho
|
||||||
|
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10]
|
||||||
|
edge [fontname="Helvetica", fontsize=8]
|
||||||
|
nodesep=0.3
|
||||||
|
ranksep=1.2
|
||||||
|
newrank=true
|
||||||
|
|
||||||
|
label="AMAR Mascotas - Data Model Overview"
|
||||||
|
labelloc="t"
|
||||||
|
fontsize=16
|
||||||
|
fontname="Helvetica-Bold"
|
||||||
|
|
||||||
|
// === ROW 1: AUTH (center) ===
|
||||||
|
subgraph cluster_auth {
|
||||||
|
label="Users & Auth"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E8F5E9"
|
||||||
|
color="#2E7D32"
|
||||||
|
auth_user [label="auth.User", fillcolor="#C8E6C9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 2: LEFT COLUMN - Pet Owners ===
|
||||||
|
subgraph cluster_mascotas {
|
||||||
|
label="Pet Owners & Pets"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E3F2FD"
|
||||||
|
color="#1565C0"
|
||||||
|
|
||||||
|
petowner [label="PetOwner", fillcolor="#BBDEFB"]
|
||||||
|
pet [label="Pet", fillcolor="#BBDEFB"]
|
||||||
|
petvaccine [label="PetVaccine", fillcolor="#90CAF9"]
|
||||||
|
petstudy [label="PetStudy", fillcolor="#90CAF9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 2: CENTER COLUMN - Veterinarians ===
|
||||||
|
subgraph cluster_vets {
|
||||||
|
label="Veterinarians"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFF3E0"
|
||||||
|
color="#E65100"
|
||||||
|
|
||||||
|
veterinarian [label="Veterinarian", fillcolor="#FFE0B2"]
|
||||||
|
availability [label="Availability", fillcolor="#FFCC80"]
|
||||||
|
unavailability [label="Unavailability", fillcolor="#FFCC80"]
|
||||||
|
vet_specialty [label="Vet-Specialty", shape=diamond, fillcolor="#FFB74D"]
|
||||||
|
vet_neighborhood [label="Vet-Neighborhood", shape=diamond, fillcolor="#FFB74D"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 2: RIGHT COLUMN - Services ===
|
||||||
|
subgraph cluster_productos {
|
||||||
|
label="Services & Pricing"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#F3E5F5"
|
||||||
|
color="#7B1FA2"
|
||||||
|
|
||||||
|
grupo [label="Group", fillcolor="#E1BEE7"]
|
||||||
|
category [label="Category", fillcolor="#E1BEE7"]
|
||||||
|
service [label="Service", fillcolor="#CE93D8"]
|
||||||
|
prices [label="Prices", fillcolor="#BA68C8"]
|
||||||
|
discounts [label="Discounts", fillcolor="#BA68C8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 3: LEFT - Cart ===
|
||||||
|
subgraph cluster_cart {
|
||||||
|
label="Cart & Checkout"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#E0F7FA"
|
||||||
|
color="#00838F"
|
||||||
|
|
||||||
|
cart [label="Cart", fillcolor="#B2EBF2"]
|
||||||
|
cartitem [label="CartItem", fillcolor="#80DEEA"]
|
||||||
|
cartresumeitem [label="CartResumeItem", fillcolor="#80DEEA"]
|
||||||
|
cartpetreason [label="CartPetReason", fillcolor="#80DEEA"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 3: CENTER - Requests ===
|
||||||
|
subgraph cluster_solicitudes {
|
||||||
|
label="Service Requests"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFEBEE"
|
||||||
|
color="#C62828"
|
||||||
|
|
||||||
|
servicerequest [label="ServiceRequest", fillcolor="#FFCDD2"]
|
||||||
|
statehistory [label="StateHistory", fillcolor="#EF9A9A"]
|
||||||
|
vetasked [label="VeterinarianAsked", fillcolor="#EF9A9A"]
|
||||||
|
vetreminder [label="VetReminder", fillcolor="#E57373"]
|
||||||
|
payreminder [label="PayReminder", fillcolor="#E57373"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 3: RIGHT - Visits ===
|
||||||
|
subgraph cluster_visits {
|
||||||
|
label="Veterinary Visits"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#FFFDE7"
|
||||||
|
color="#F9A825"
|
||||||
|
|
||||||
|
vetvisit [label="VetVisit", fillcolor="#FFF9C4"]
|
||||||
|
vetvisitreport [label="VetVisitReport", fillcolor="#FFF59D"]
|
||||||
|
vetvisitfollowup [label="FollowUp", fillcolor="#FFF176"]
|
||||||
|
vetvisitpetreason [label="VisitPetReason", fillcolor="#FFF176"]
|
||||||
|
visit_pets [label="Visit-Pets", shape=diamond, fillcolor="#FFEE58"]
|
||||||
|
receipt [label="Receipt\n(AFIP)", fillcolor="#F8BBD9"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ROW 4: REFERENCE DATA (bottom, full width) ===
|
||||||
|
subgraph cluster_reference {
|
||||||
|
label="Reference Data"
|
||||||
|
style="rounded,filled"
|
||||||
|
fillcolor="#ECEFF1"
|
||||||
|
color="#455A64"
|
||||||
|
|
||||||
|
subgraph {
|
||||||
|
rank=same
|
||||||
|
specialty [label="Specialty", fillcolor="#CFD8DC"]
|
||||||
|
neighborhood [label="Neighborhood", fillcolor="#CFD8DC"]
|
||||||
|
province [label="Province", fillcolor="#CFD8DC"]
|
||||||
|
locality [label="Locality", fillcolor="#CFD8DC"]
|
||||||
|
petbreed [label="PetBreed", fillcolor="#CFD8DC"]
|
||||||
|
vaccine [label="Vaccine", fillcolor="#CFD8DC"]
|
||||||
|
}
|
||||||
|
subgraph {
|
||||||
|
rank=same
|
||||||
|
study [label="Study", fillcolor="#CFD8DC"]
|
||||||
|
campaign [label="Campaign", fillcolor="#CFD8DC"]
|
||||||
|
tag [label="Tag", fillcolor="#CFD8DC"]
|
||||||
|
medication [label="Medication", fillcolor="#CFD8DC"]
|
||||||
|
turnfeegroup [label="TurnFeeGroup", fillcolor="#CFD8DC"]
|
||||||
|
}
|
||||||
|
specialty -> study [style=invis]
|
||||||
|
province -> locality
|
||||||
|
}
|
||||||
|
|
||||||
|
// === FORCE COLUMN ALIGNMENT WITH INVISIBLE EDGES ===
|
||||||
|
// Column 1: Pets -> Cart
|
||||||
|
petowner -> cart [style=invis, weight=10]
|
||||||
|
// Column 2: Vets -> Requests
|
||||||
|
veterinarian -> servicerequest [style=invis, weight=10]
|
||||||
|
// Column 3: Services -> Visits
|
||||||
|
service -> vetvisit [style=invis, weight=10]
|
||||||
|
|
||||||
|
// Force Reference Data to bottom
|
||||||
|
cart -> specialty [style=invis, weight=10]
|
||||||
|
servicerequest -> campaign [style=invis, weight=10]
|
||||||
|
vetvisit -> turnfeegroup [style=invis, weight=10]
|
||||||
|
|
||||||
|
// === INTERNAL CLUSTER EDGES ===
|
||||||
|
petowner -> pet [label="1:N"]
|
||||||
|
pet -> petvaccine [label="1:N"]
|
||||||
|
pet -> petstudy [label="1:N"]
|
||||||
|
|
||||||
|
veterinarian -> availability
|
||||||
|
veterinarian -> unavailability
|
||||||
|
veterinarian -> vet_specialty
|
||||||
|
veterinarian -> vet_neighborhood
|
||||||
|
|
||||||
|
grupo -> category [label="1:N"]
|
||||||
|
category -> service [label="1:N"]
|
||||||
|
service -> prices [label="1:N"]
|
||||||
|
service -> discounts [label="1:N"]
|
||||||
|
|
||||||
|
cart -> cartitem [label="1:N"]
|
||||||
|
cart -> cartresumeitem
|
||||||
|
cart -> cartpetreason
|
||||||
|
|
||||||
|
servicerequest -> statehistory [label="1:N"]
|
||||||
|
servicerequest -> vetasked [label="1:N"]
|
||||||
|
servicerequest -> payreminder
|
||||||
|
vetasked -> vetreminder
|
||||||
|
|
||||||
|
vetvisit -> visit_pets
|
||||||
|
vetvisit -> vetvisitreport [label="1:N"]
|
||||||
|
vetvisit -> vetvisitpetreason
|
||||||
|
vetvisitreport -> vetvisitfollowup
|
||||||
|
vetvisit -> receipt [style=dashed]
|
||||||
|
|
||||||
|
// === CROSS-CLUSTER RELATIONSHIPS ===
|
||||||
|
auth_user -> petowner [label="1:1 opt", style=dashed]
|
||||||
|
auth_user -> veterinarian [label="1:1"]
|
||||||
|
|
||||||
|
petowner -> cart [label="1:N"]
|
||||||
|
petowner -> servicerequest [label="1:N"]
|
||||||
|
servicerequest -> cart [label="1:1", constraint=false]
|
||||||
|
vetasked -> veterinarian [constraint=false]
|
||||||
|
vetvisit -> servicerequest [style=dashed, constraint=false]
|
||||||
|
|
||||||
|
// Reference links
|
||||||
|
petvaccine -> vaccine [constraint=false]
|
||||||
|
petstudy -> study [constraint=false]
|
||||||
|
vet_specialty -> specialty [constraint=false]
|
||||||
|
vet_neighborhood -> neighborhood [constraint=false]
|
||||||
|
cartitem -> service [constraint=false]
|
||||||
|
}
|
||||||
540
cfg/amar/soleprint/atlas/books/arch/04-data-model-simple.svg
Normal file
540
cfg/amar/soleprint/atlas/books/arch/04-data-model-simple.svg
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.0.5 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: DataModelSimple Pages: 1 -->
|
||||||
|
<svg width="2433pt" height="780pt"
|
||||||
|
viewBox="0.00 0.00 2433.00 780.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 776.4)">
|
||||||
|
<title>DataModelSimple</title>
|
||||||
|
<polygon fill="white" stroke="none" points="-4,4 -4,-776.4 2429,-776.4 2429,4 -4,4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1212.5" y="-753.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas - Data Model Overview</text>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_auth</title>
|
||||||
|
<path fill="#e8f5e9" stroke="#2e7d32" d="M890,-658C890,-658 975,-658 975,-658 981,-658 987,-664 987,-670 987,-670 987,-725.2 987,-725.2 987,-731.2 981,-737.2 975,-737.2 975,-737.2 890,-737.2 890,-737.2 884,-737.2 878,-731.2 878,-725.2 878,-725.2 878,-670 878,-670 878,-664 884,-658 890,-658"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="932.5" y="-718" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Users & Auth</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_mascotas</title>
|
||||||
|
<path fill="#e3f2fd" stroke="#1565c0" d="M1662,-262.6C1662,-262.6 1799,-262.6 1799,-262.6 1805,-262.6 1811,-268.6 1811,-274.6 1811,-274.6 1811,-593.4 1811,-593.4 1811,-599.4 1805,-605.4 1799,-605.4 1799,-605.4 1662,-605.4 1662,-605.4 1656,-605.4 1650,-599.4 1650,-593.4 1650,-593.4 1650,-274.6 1650,-274.6 1650,-268.6 1656,-262.6 1662,-262.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1730.5" y="-586.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Pet Owners & Pets</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust3" class="cluster">
|
||||||
|
<title>cluster_vets</title>
|
||||||
|
<path fill="#fff3e0" stroke="#e65100" d="M20,-262.6C20,-262.6 509,-262.6 509,-262.6 515,-262.6 521,-268.6 521,-274.6 521,-274.6 521,-461.6 521,-461.6 521,-467.6 515,-473.6 509,-473.6 509,-473.6 20,-473.6 20,-473.6 14,-473.6 8,-467.6 8,-461.6 8,-461.6 8,-274.6 8,-274.6 8,-268.6 14,-262.6 20,-262.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="264.5" y="-454.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinarians</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust4" class="cluster">
|
||||||
|
<title>cluster_productos</title>
|
||||||
|
<path fill="#f3e5f5" stroke="#7b1fa2" d="M2277,-262.6C2277,-262.6 2405,-262.6 2405,-262.6 2411,-262.6 2417,-268.6 2417,-274.6 2417,-274.6 2417,-725.2 2417,-725.2 2417,-731.2 2411,-737.2 2405,-737.2 2405,-737.2 2277,-737.2 2277,-737.2 2271,-737.2 2265,-731.2 2265,-725.2 2265,-725.2 2265,-274.6 2265,-274.6 2265,-268.6 2271,-262.6 2277,-262.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2341" y="-718" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services & Pricing</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust5" class="cluster">
|
||||||
|
<title>cluster_cart</title>
|
||||||
|
<path fill="#e0f7fa" stroke="#00838f" d="M853,-262.6C853,-262.6 1119,-262.6 1119,-262.6 1125,-262.6 1131,-268.6 1131,-274.6 1131,-274.6 1131,-461.6 1131,-461.6 1131,-467.6 1125,-473.6 1119,-473.6 1119,-473.6 853,-473.6 853,-473.6 847,-473.6 841,-467.6 841,-461.6 841,-461.6 841,-274.6 841,-274.6 841,-268.6 847,-262.6 853,-262.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="986" y="-454.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Cart & Checkout</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust6" class="cluster">
|
||||||
|
<title>cluster_solicitudes</title>
|
||||||
|
<path fill="#ffebee" stroke="#c62828" d="M541,-8C541,-8 821,-8 821,-8 827,-8 833,-14 833,-20 833,-20 833,-329.8 833,-329.8 833,-335.8 827,-341.8 821,-341.8 821,-341.8 541,-341.8 541,-341.8 535,-341.8 529,-335.8 529,-329.8 529,-329.8 529,-20 529,-20 529,-14 535,-8 541,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="681" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Service Requests</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust7" class="cluster">
|
||||||
|
<title>cluster_visits</title>
|
||||||
|
<path fill="#fffde7" stroke="#f9a825" d="M1869,-8C1869,-8 2245,-8 2245,-8 2251,-8 2257,-14 2257,-20 2257,-20 2257,-329.8 2257,-329.8 2257,-335.8 2251,-341.8 2245,-341.8 2245,-341.8 1869,-341.8 1869,-341.8 1863,-341.8 1857,-335.8 1857,-329.8 1857,-329.8 1857,-20 1857,-20 1857,-14 1863,-8 1869,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2057" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinary Visits</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust8" class="cluster">
|
||||||
|
<title>cluster_reference</title>
|
||||||
|
<path fill="#eceff1" stroke="#455a64" d="M1151,-131C1151,-131 1630,-131 1630,-131 1636,-131 1642,-137 1642,-143 1642,-143 1642,-329.8 1642,-329.8 1642,-335.8 1636,-341.8 1630,-341.8 1630,-341.8 1151,-341.8 1151,-341.8 1145,-341.8 1139,-335.8 1139,-329.8 1139,-329.8 1139,-143 1139,-143 1139,-137 1145,-131 1151,-131"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1390.5" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reference Data</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>auth_user</title>
|
||||||
|
<path fill="#c8e6c9" stroke="black" d="M949.68,-702C949.68,-702 914.32,-702 914.32,-702 908.32,-702 902.32,-696 902.32,-690 902.32,-690 902.32,-678 902.32,-678 902.32,-672 908.32,-666 914.32,-666 914.32,-666 949.68,-666 949.68,-666 955.68,-666 961.68,-672 961.68,-678 961.68,-678 961.68,-690 961.68,-690 961.68,-696 955.68,-702 949.68,-702"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="932" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00">auth.User</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>petowner</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M1706.23,-570.2C1706.23,-570.2 1669.77,-570.2 1669.77,-570.2 1663.77,-570.2 1657.77,-564.2 1657.77,-558.2 1657.77,-558.2 1657.77,-546.2 1657.77,-546.2 1657.77,-540.2 1663.77,-534.2 1669.77,-534.2 1669.77,-534.2 1706.23,-534.2 1706.23,-534.2 1712.23,-534.2 1718.23,-540.2 1718.23,-546.2 1718.23,-546.2 1718.23,-558.2 1718.23,-558.2 1718.23,-564.2 1712.23,-570.2 1706.23,-570.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1688" y="-549.2" font-family="Helvetica,sans-Serif" font-size="10.00">PetOwner</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user->petowner -->
|
||||||
|
<g id="edge32" class="edge">
|
||||||
|
<title>auth_user->petowner</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M943.44,-665.62C943.44,-631.77 943.44,-563 943.44,-563 943.44,-563 1645.91,-563 1645.91,-563"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1645.91,-566.5 1655.91,-563 1645.91,-559.5 1645.91,-566.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1213.23" y="-615.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>veterinarian</title>
|
||||||
|
<path fill="#ffe0b2" stroke="black" d="M500.96,-438.4C500.96,-438.4 455.04,-438.4 455.04,-438.4 449.04,-438.4 443.04,-432.4 443.04,-426.4 443.04,-426.4 443.04,-414.4 443.04,-414.4 443.04,-408.4 449.04,-402.4 455.04,-402.4 455.04,-402.4 500.96,-402.4 500.96,-402.4 506.96,-402.4 512.96,-408.4 512.96,-414.4 512.96,-414.4 512.96,-426.4 512.96,-426.4 512.96,-432.4 506.96,-438.4 500.96,-438.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="478" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Veterinarian</text>
|
||||||
|
</g>
|
||||||
|
<!-- auth_user->veterinarian -->
|
||||||
|
<g id="edge33" class="edge">
|
||||||
|
<title>auth_user->veterinarian</title>
|
||||||
|
<path fill="none" stroke="black" d="M914.19,-665.54C914.19,-607.58 914.19,-433 914.19,-433 914.19,-433 524.84,-433 524.84,-433"/>
|
||||||
|
<polygon fill="black" stroke="black" points="524.84,-429.5 514.84,-433 524.84,-436.5 524.84,-429.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="667.56" y="-549.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>pet</title>
|
||||||
|
<path fill="#bbdefb" stroke="black" d="M1706,-438.4C1706,-438.4 1676,-438.4 1676,-438.4 1670,-438.4 1664,-432.4 1664,-426.4 1664,-426.4 1664,-414.4 1664,-414.4 1664,-408.4 1670,-402.4 1676,-402.4 1676,-402.4 1706,-402.4 1706,-402.4 1712,-402.4 1718,-408.4 1718,-414.4 1718,-414.4 1718,-426.4 1718,-426.4 1718,-432.4 1712,-438.4 1706,-438.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1691" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Pet</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->pet -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>petowner->pet</title>
|
||||||
|
<path fill="none" stroke="black" d="M1691,-533.87C1691,-533.87 1691,-450.08 1691,-450.08"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1694.5,-450.08 1691,-440.08 1687.5,-450.08 1694.5,-450.08"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1696.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart -->
|
||||||
|
<g id="node16" class="node">
|
||||||
|
<title>cart</title>
|
||||||
|
<path fill="#b2ebf2" stroke="black" d="M1111,-438.4C1111,-438.4 1081,-438.4 1081,-438.4 1075,-438.4 1069,-432.4 1069,-426.4 1069,-426.4 1069,-414.4 1069,-414.4 1069,-408.4 1075,-402.4 1081,-402.4 1081,-402.4 1111,-402.4 1111,-402.4 1117,-402.4 1123,-408.4 1123,-414.4 1123,-414.4 1123,-426.4 1123,-426.4 1123,-432.4 1117,-438.4 1111,-438.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1096" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Cart</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->cart -->
|
||||||
|
<!-- petowner->cart -->
|
||||||
|
<g id="edge34" class="edge">
|
||||||
|
<title>petowner->cart</title>
|
||||||
|
<path fill="none" stroke="black" d="M1657.48,-541C1537.52,-541 1105,-541 1105,-541 1105,-541 1105,-450.23 1105,-450.23"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1108.5,-450.23 1105,-440.23 1101.5,-450.23 1108.5,-450.23"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1233.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest -->
|
||||||
|
<g id="node20" class="node">
|
||||||
|
<title>servicerequest</title>
|
||||||
|
<path fill="#ffcdd2" stroke="black" d="M763.3,-306.6C763.3,-306.6 700.7,-306.6 700.7,-306.6 694.7,-306.6 688.7,-300.6 688.7,-294.6 688.7,-294.6 688.7,-282.6 688.7,-282.6 688.7,-276.6 694.7,-270.6 700.7,-270.6 700.7,-270.6 763.3,-270.6 763.3,-270.6 769.3,-270.6 775.3,-276.6 775.3,-282.6 775.3,-282.6 775.3,-294.6 775.3,-294.6 775.3,-300.6 769.3,-306.6 763.3,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="732" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceRequest</text>
|
||||||
|
</g>
|
||||||
|
<!-- petowner->servicerequest -->
|
||||||
|
<g id="edge35" class="edge">
|
||||||
|
<title>petowner->servicerequest</title>
|
||||||
|
<path fill="none" stroke="black" d="M1657.27,-556C1493.64,-556 732,-556 732,-556 732,-556 732,-318.49 732,-318.49"/>
|
||||||
|
<polygon fill="black" stroke="black" points="735.5,-318.49 732,-308.49 728.5,-318.49 735.5,-318.49"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="782.22" y="-418" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- petvaccine -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>petvaccine</title>
|
||||||
|
<path fill="#90caf9" stroke="black" d="M1712.29,-306.6C1712.29,-306.6 1669.71,-306.6 1669.71,-306.6 1663.71,-306.6 1657.71,-300.6 1657.71,-294.6 1657.71,-294.6 1657.71,-282.6 1657.71,-282.6 1657.71,-276.6 1663.71,-270.6 1669.71,-270.6 1669.71,-270.6 1712.29,-270.6 1712.29,-270.6 1718.29,-270.6 1724.29,-276.6 1724.29,-282.6 1724.29,-282.6 1724.29,-294.6 1724.29,-294.6 1724.29,-300.6 1718.29,-306.6 1712.29,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1691" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetVaccine</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->petvaccine -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>pet->petvaccine</title>
|
||||||
|
<path fill="none" stroke="black" d="M1691,-402.07C1691,-402.07 1691,-318.28 1691,-318.28"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1694.5,-318.28 1691,-308.28 1687.5,-318.28 1694.5,-318.28"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1697.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- petstudy -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>petstudy</title>
|
||||||
|
<path fill="#90caf9" stroke="black" d="M1791.29,-306.6C1791.29,-306.6 1758.71,-306.6 1758.71,-306.6 1752.71,-306.6 1746.71,-300.6 1746.71,-294.6 1746.71,-294.6 1746.71,-282.6 1746.71,-282.6 1746.71,-276.6 1752.71,-270.6 1758.71,-270.6 1758.71,-270.6 1791.29,-270.6 1791.29,-270.6 1797.29,-270.6 1803.29,-276.6 1803.29,-282.6 1803.29,-282.6 1803.29,-294.6 1803.29,-294.6 1803.29,-300.6 1797.29,-306.6 1791.29,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1775" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetStudy</text>
|
||||||
|
</g>
|
||||||
|
<!-- pet->petstudy -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>pet->petstudy</title>
|
||||||
|
<path fill="none" stroke="black" d="M1718.04,-414C1742.6,-414 1775,-414 1775,-414 1775,-414 1775,-318.35 1775,-318.35"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1778.5,-318.35 1775,-308.35 1771.5,-318.35 1778.5,-318.35"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1758.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vaccine -->
|
||||||
|
<g id="node36" class="node">
|
||||||
|
<title>vaccine</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1189,-306.6C1189,-306.6 1159,-306.6 1159,-306.6 1153,-306.6 1147,-300.6 1147,-294.6 1147,-294.6 1147,-282.6 1147,-282.6 1147,-276.6 1153,-270.6 1159,-270.6 1159,-270.6 1189,-270.6 1189,-270.6 1195,-270.6 1201,-276.6 1201,-282.6 1201,-282.6 1201,-294.6 1201,-294.6 1201,-300.6 1195,-306.6 1189,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1174" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vaccine</text>
|
||||||
|
</g>
|
||||||
|
<!-- petvaccine->vaccine -->
|
||||||
|
<g id="edge39" class="edge">
|
||||||
|
<title>petvaccine->vaccine</title>
|
||||||
|
<path fill="none" stroke="black" d="M1691,-270.32C1691,-257.63 1691,-243 1691,-243 1691,-243 1174.1,-243 1174.1,-243 1174.1,-243 1174.1,-258.8 1174.1,-258.8"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1170.6,-258.8 1174.1,-268.8 1177.6,-258.8 1170.6,-258.8"/>
|
||||||
|
</g>
|
||||||
|
<!-- study -->
|
||||||
|
<g id="node37" class="node">
|
||||||
|
<title>study</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1356,-175C1356,-175 1326,-175 1326,-175 1320,-175 1314,-169 1314,-163 1314,-163 1314,-151 1314,-151 1314,-145 1320,-139 1326,-139 1326,-139 1356,-139 1356,-139 1362,-139 1368,-145 1368,-151 1368,-151 1368,-163 1368,-163 1368,-169 1362,-175 1356,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1341" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Study</text>
|
||||||
|
</g>
|
||||||
|
<!-- petstudy->study -->
|
||||||
|
<g id="edge40" class="edge">
|
||||||
|
<title>petstudy->study</title>
|
||||||
|
<path fill="none" stroke="black" d="M1775,-270.23C1775,-244.81 1775,-202 1775,-202 1775,-202 1353.18,-202 1353.18,-202 1353.18,-202 1353.18,-186.82 1353.18,-186.82"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1356.68,-186.82 1353.18,-176.82 1349.68,-186.82 1356.68,-186.82"/>
|
||||||
|
</g>
|
||||||
|
<!-- availability -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>availability</title>
|
||||||
|
<path fill="#ffcc80" stroke="black" d="M67.62,-306.6C67.62,-306.6 28.38,-306.6 28.38,-306.6 22.38,-306.6 16.38,-300.6 16.38,-294.6 16.38,-294.6 16.38,-282.6 16.38,-282.6 16.38,-276.6 22.38,-270.6 28.38,-270.6 28.38,-270.6 67.62,-270.6 67.62,-270.6 73.62,-270.6 79.62,-276.6 79.62,-282.6 79.62,-282.6 79.62,-294.6 79.62,-294.6 79.62,-300.6 73.62,-306.6 67.62,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="48" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Availability</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->availability -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>veterinarian->availability</title>
|
||||||
|
<path fill="none" stroke="black" d="M442.81,-429C340.11,-429 48,-429 48,-429 48,-429 48,-318.47 48,-318.47"/>
|
||||||
|
<polygon fill="black" stroke="black" points="51.5,-318.47 48,-308.47 44.5,-318.47 51.5,-318.47"/>
|
||||||
|
</g>
|
||||||
|
<!-- unavailability -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>unavailability</title>
|
||||||
|
<path fill="#ffcc80" stroke="black" d="M164.46,-306.6C164.46,-306.6 113.54,-306.6 113.54,-306.6 107.54,-306.6 101.54,-300.6 101.54,-294.6 101.54,-294.6 101.54,-282.6 101.54,-282.6 101.54,-276.6 107.54,-270.6 113.54,-270.6 113.54,-270.6 164.46,-270.6 164.46,-270.6 170.46,-270.6 176.46,-276.6 176.46,-282.6 176.46,-282.6 176.46,-294.6 176.46,-294.6 176.46,-300.6 170.46,-306.6 164.46,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="139" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Unavailability</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->unavailability -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>veterinarian->unavailability</title>
|
||||||
|
<path fill="none" stroke="black" d="M442.68,-420C355.85,-420 139,-420 139,-420 139,-420 139,-318.38 139,-318.38"/>
|
||||||
|
<polygon fill="black" stroke="black" points="142.5,-318.38 139,-308.38 135.5,-318.38 142.5,-318.38"/>
|
||||||
|
</g>
|
||||||
|
<!-- vet_specialty -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>vet_specialty</title>
|
||||||
|
<path fill="#ffb74d" stroke="black" d="M250.45,-303.34C250.45,-303.34 209.84,-291.86 209.84,-291.86 204.06,-290.23 204.06,-286.97 209.84,-285.34 209.84,-285.34 250.45,-273.86 250.45,-273.86 256.23,-272.23 267.77,-272.23 273.55,-273.86 273.55,-273.86 314.16,-285.34 314.16,-285.34 319.94,-286.97 319.94,-290.23 314.16,-291.86 314.16,-291.86 273.55,-303.34 273.55,-303.34 267.77,-304.97 256.23,-304.97 250.45,-303.34"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="262" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vet-Specialty</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->vet_specialty -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>veterinarian->vet_specialty</title>
|
||||||
|
<path fill="none" stroke="black" d="M442.82,-411C381.61,-411 262,-411 262,-411 262,-411 262,-318.44 262,-318.44"/>
|
||||||
|
<polygon fill="black" stroke="black" points="265.5,-318.44 262,-308.44 258.5,-318.44 265.5,-318.44"/>
|
||||||
|
</g>
|
||||||
|
<!-- vet_neighborhood -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>vet_neighborhood</title>
|
||||||
|
<path fill="#ffb74d" stroke="black" d="M418.27,-304.05C418.27,-304.05 359.09,-291.15 359.09,-291.15 353.23,-289.88 353.23,-287.32 359.09,-286.05 359.09,-286.05 418.27,-273.15 418.27,-273.15 424.14,-271.88 435.86,-271.88 441.73,-273.15 441.73,-273.15 500.91,-286.05 500.91,-286.05 506.77,-287.32 506.77,-289.88 500.91,-291.15 500.91,-291.15 441.73,-304.05 441.73,-304.05 435.86,-305.32 424.14,-305.32 418.27,-304.05"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="430" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vet-Neighborhood</text>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->vet_neighborhood -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>veterinarian->vet_neighborhood</title>
|
||||||
|
<path fill="none" stroke="black" d="M466.24,-402.07C466.24,-402.07 466.24,-310.63 466.24,-310.63"/>
|
||||||
|
<polygon fill="black" stroke="black" points="469.74,-310.63 466.24,-300.63 462.74,-310.63 469.74,-310.63"/>
|
||||||
|
</g>
|
||||||
|
<!-- veterinarian->servicerequest -->
|
||||||
|
<!-- specialty -->
|
||||||
|
<g id="node31" class="node">
|
||||||
|
<title>specialty</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1622.29,-306.6C1622.29,-306.6 1589.71,-306.6 1589.71,-306.6 1583.71,-306.6 1577.71,-300.6 1577.71,-294.6 1577.71,-294.6 1577.71,-282.6 1577.71,-282.6 1577.71,-276.6 1583.71,-270.6 1589.71,-270.6 1589.71,-270.6 1622.29,-270.6 1622.29,-270.6 1628.29,-270.6 1634.29,-276.6 1634.29,-282.6 1634.29,-282.6 1634.29,-294.6 1634.29,-294.6 1634.29,-300.6 1628.29,-306.6 1622.29,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1606" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Specialty</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_specialty->specialty -->
|
||||||
|
<g id="edge41" class="edge">
|
||||||
|
<title>vet_specialty->specialty</title>
|
||||||
|
<path fill="none" stroke="black" d="M262,-270.22C262,-263.27 262,-257 262,-257 262,-257 1591.73,-257 1591.73,-257 1591.73,-257 1591.73,-258.7 1591.73,-258.7"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1588.23,-258.7 1591.73,-268.7 1595.23,-258.7 1588.23,-258.7"/>
|
||||||
|
</g>
|
||||||
|
<!-- neighborhood -->
|
||||||
|
<g id="node32" class="node">
|
||||||
|
<title>neighborhood</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1289.41,-306.6C1289.41,-306.6 1234.59,-306.6 1234.59,-306.6 1228.59,-306.6 1222.59,-300.6 1222.59,-294.6 1222.59,-294.6 1222.59,-282.6 1222.59,-282.6 1222.59,-276.6 1228.59,-270.6 1234.59,-270.6 1234.59,-270.6 1289.41,-270.6 1289.41,-270.6 1295.41,-270.6 1301.41,-276.6 1301.41,-282.6 1301.41,-282.6 1301.41,-294.6 1301.41,-294.6 1301.41,-300.6 1295.41,-306.6 1289.41,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1262" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Neighborhood</text>
|
||||||
|
</g>
|
||||||
|
<!-- vet_neighborhood->neighborhood -->
|
||||||
|
<g id="edge42" class="edge">
|
||||||
|
<title>vet_neighborhood->neighborhood</title>
|
||||||
|
<path fill="none" stroke="black" d="M489.44,-293.99C489.44,-307 489.44,-339 489.44,-339 489.44,-339 1262,-339 1262,-339 1262,-339 1262,-318.59 1262,-318.59"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1265.5,-318.59 1262,-308.59 1258.5,-318.59 1265.5,-318.59"/>
|
||||||
|
</g>
|
||||||
|
<!-- grupo -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>grupo</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M2316,-702C2316,-702 2286,-702 2286,-702 2280,-702 2274,-696 2274,-690 2274,-690 2274,-678 2274,-678 2274,-672 2280,-666 2286,-666 2286,-666 2316,-666 2316,-666 2322,-666 2328,-672 2328,-678 2328,-678 2328,-690 2328,-690 2328,-696 2322,-702 2316,-702"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2301" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00">Group</text>
|
||||||
|
</g>
|
||||||
|
<!-- category -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>category</title>
|
||||||
|
<path fill="#e1bee7" stroke="black" d="M2317.29,-570.2C2317.29,-570.2 2284.71,-570.2 2284.71,-570.2 2278.71,-570.2 2272.71,-564.2 2272.71,-558.2 2272.71,-558.2 2272.71,-546.2 2272.71,-546.2 2272.71,-540.2 2278.71,-534.2 2284.71,-534.2 2284.71,-534.2 2317.29,-534.2 2317.29,-534.2 2323.29,-534.2 2329.29,-540.2 2329.29,-546.2 2329.29,-546.2 2329.29,-558.2 2329.29,-558.2 2329.29,-564.2 2323.29,-570.2 2317.29,-570.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2301" y="-549.2" font-family="Helvetica,sans-Serif" font-size="10.00">Category</text>
|
||||||
|
</g>
|
||||||
|
<!-- grupo->category -->
|
||||||
|
<g id="edge16" class="edge">
|
||||||
|
<title>grupo->category</title>
|
||||||
|
<path fill="none" stroke="black" d="M2301,-665.67C2301,-665.67 2301,-581.88 2301,-581.88"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2304.5,-581.88 2301,-571.88 2297.5,-581.88 2304.5,-581.88"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2307.22" y="-615.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- service -->
|
||||||
|
<g id="node13" class="node">
|
||||||
|
<title>service</title>
|
||||||
|
<path fill="#ce93d8" stroke="black" d="M2315,-438.4C2315,-438.4 2285,-438.4 2285,-438.4 2279,-438.4 2273,-432.4 2273,-426.4 2273,-426.4 2273,-414.4 2273,-414.4 2273,-408.4 2279,-402.4 2285,-402.4 2285,-402.4 2315,-402.4 2315,-402.4 2321,-402.4 2327,-408.4 2327,-414.4 2327,-414.4 2327,-426.4 2327,-426.4 2327,-432.4 2321,-438.4 2315,-438.4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2300" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
|
||||||
|
</g>
|
||||||
|
<!-- category->service -->
|
||||||
|
<g id="edge17" class="edge">
|
||||||
|
<title>category->service</title>
|
||||||
|
<path fill="none" stroke="black" d="M2300,-533.87C2300,-533.87 2300,-450.08 2300,-450.08"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2303.5,-450.08 2300,-440.08 2296.5,-450.08 2303.5,-450.08"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2306.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- prices -->
|
||||||
|
<g id="node14" class="node">
|
||||||
|
<title>prices</title>
|
||||||
|
<path fill="#ba68c8" stroke="black" d="M2315,-306.6C2315,-306.6 2285,-306.6 2285,-306.6 2279,-306.6 2273,-300.6 2273,-294.6 2273,-294.6 2273,-282.6 2273,-282.6 2273,-276.6 2279,-270.6 2285,-270.6 2285,-270.6 2315,-270.6 2315,-270.6 2321,-270.6 2327,-276.6 2327,-282.6 2327,-282.6 2327,-294.6 2327,-294.6 2327,-300.6 2321,-306.6 2315,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2300" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Prices</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->prices -->
|
||||||
|
<g id="edge18" class="edge">
|
||||||
|
<title>service->prices</title>
|
||||||
|
<path fill="none" stroke="black" d="M2309,-402.07C2309,-402.07 2309,-318.28 2309,-318.28"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2312.5,-318.28 2309,-308.28 2305.5,-318.28 2312.5,-318.28"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2306.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- discounts -->
|
||||||
|
<g id="node15" class="node">
|
||||||
|
<title>discounts</title>
|
||||||
|
<path fill="#ba68c8" stroke="black" d="M2396.95,-306.6C2396.95,-306.6 2361.05,-306.6 2361.05,-306.6 2355.05,-306.6 2349.05,-300.6 2349.05,-294.6 2349.05,-294.6 2349.05,-282.6 2349.05,-282.6 2349.05,-276.6 2355.05,-270.6 2361.05,-270.6 2361.05,-270.6 2396.95,-270.6 2396.95,-270.6 2402.95,-270.6 2408.95,-276.6 2408.95,-282.6 2408.95,-282.6 2408.95,-294.6 2408.95,-294.6 2408.95,-300.6 2402.95,-306.6 2396.95,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2379" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Discounts</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->discounts -->
|
||||||
|
<g id="edge19" class="edge">
|
||||||
|
<title>service->discounts</title>
|
||||||
|
<path fill="none" stroke="black" d="M2327.19,-420C2350.04,-420 2379,-420 2379,-420 2379,-420 2379,-318.38 2379,-318.38"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2382.5,-318.38 2379,-308.38 2375.5,-318.38 2382.5,-318.38"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2365.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit -->
|
||||||
|
<g id="node25" class="node">
|
||||||
|
<title>vetvisit</title>
|
||||||
|
<path fill="#fff9c4" stroke="black" d="M2081,-306.6C2081,-306.6 2051,-306.6 2051,-306.6 2045,-306.6 2039,-300.6 2039,-294.6 2039,-294.6 2039,-282.6 2039,-282.6 2039,-276.6 2045,-270.6 2051,-270.6 2051,-270.6 2081,-270.6 2081,-270.6 2087,-270.6 2093,-276.6 2093,-282.6 2093,-282.6 2093,-294.6 2093,-294.6 2093,-300.6 2087,-306.6 2081,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2066" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisit</text>
|
||||||
|
</g>
|
||||||
|
<!-- service->vetvisit -->
|
||||||
|
<!-- cartitem -->
|
||||||
|
<g id="node17" class="node">
|
||||||
|
<title>cartitem</title>
|
||||||
|
<path fill="#80deea" stroke="black" d="M891.17,-306.6C891.17,-306.6 860.83,-306.6 860.83,-306.6 854.83,-306.6 848.83,-300.6 848.83,-294.6 848.83,-294.6 848.83,-282.6 848.83,-282.6 848.83,-276.6 854.83,-270.6 860.83,-270.6 860.83,-270.6 891.17,-270.6 891.17,-270.6 897.17,-270.6 903.17,-276.6 903.17,-282.6 903.17,-282.6 903.17,-294.6 903.17,-294.6 903.17,-300.6 897.17,-306.6 891.17,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="876" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartItem</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->cartitem -->
|
||||||
|
<g id="edge20" class="edge">
|
||||||
|
<title>cart->cartitem</title>
|
||||||
|
<path fill="none" stroke="black" d="M1068.78,-413C1011.66,-413 884.49,-413 884.49,-413 884.49,-413 884.49,-318.47 884.49,-318.47"/>
|
||||||
|
<polygon fill="black" stroke="black" points="887.99,-318.47 884.49,-308.47 880.99,-318.47 887.99,-318.47"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="924.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- cartresumeitem -->
|
||||||
|
<g id="node18" class="node">
|
||||||
|
<title>cartresumeitem</title>
|
||||||
|
<path fill="#80deea" stroke="black" d="M1004.79,-306.6C1004.79,-306.6 937.21,-306.6 937.21,-306.6 931.21,-306.6 925.21,-300.6 925.21,-294.6 925.21,-294.6 925.21,-282.6 925.21,-282.6 925.21,-276.6 931.21,-270.6 937.21,-270.6 937.21,-270.6 1004.79,-270.6 1004.79,-270.6 1010.79,-270.6 1016.79,-276.6 1016.79,-282.6 1016.79,-282.6 1016.79,-294.6 1016.79,-294.6 1016.79,-300.6 1010.79,-306.6 1004.79,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="971" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartResumeItem</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->cartresumeitem -->
|
||||||
|
<g id="edge21" class="edge">
|
||||||
|
<title>cart->cartresumeitem</title>
|
||||||
|
<path fill="none" stroke="black" d="M1068.5,-408C1036.87,-408 989.23,-408 989.23,-408 989.23,-408 989.23,-318.51 989.23,-318.51"/>
|
||||||
|
<polygon fill="black" stroke="black" points="992.73,-318.51 989.23,-308.51 985.73,-318.51 992.73,-318.51"/>
|
||||||
|
</g>
|
||||||
|
<!-- cartpetreason -->
|
||||||
|
<g id="node19" class="node">
|
||||||
|
<title>cartpetreason</title>
|
||||||
|
<path fill="#80deea" stroke="black" d="M1111.18,-306.6C1111.18,-306.6 1050.82,-306.6 1050.82,-306.6 1044.82,-306.6 1038.82,-300.6 1038.82,-294.6 1038.82,-294.6 1038.82,-282.6 1038.82,-282.6 1038.82,-276.6 1044.82,-270.6 1050.82,-270.6 1050.82,-270.6 1111.18,-270.6 1111.18,-270.6 1117.18,-270.6 1123.18,-276.6 1123.18,-282.6 1123.18,-282.6 1123.18,-294.6 1123.18,-294.6 1123.18,-300.6 1117.18,-306.6 1111.18,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1081" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartPetReason</text>
|
||||||
|
</g>
|
||||||
|
<!-- cart->cartpetreason -->
|
||||||
|
<g id="edge22" class="edge">
|
||||||
|
<title>cart->cartpetreason</title>
|
||||||
|
<path fill="none" stroke="black" d="M1096,-402.07C1096,-402.07 1096,-318.28 1096,-318.28"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1099.5,-318.28 1096,-308.28 1092.5,-318.28 1099.5,-318.28"/>
|
||||||
|
</g>
|
||||||
|
<!-- cart->specialty -->
|
||||||
|
<!-- cartitem->service -->
|
||||||
|
<g id="edge43" class="edge">
|
||||||
|
<title>cartitem->service</title>
|
||||||
|
<path fill="none" stroke="black" d="M866.66,-307.07C866.66,-331.09 866.66,-370 866.66,-370 866.66,-370 2291,-370 2291,-370 2291,-370 2291,-390.41 2291,-390.41"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2287.5,-390.41 2291,-400.41 2294.5,-390.41 2287.5,-390.41"/>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->cart -->
|
||||||
|
<g id="edge36" class="edge">
|
||||||
|
<title>servicerequest->cart</title>
|
||||||
|
<path fill="none" stroke="black" d="M753.65,-306.82C753.65,-344.61 753.65,-428 753.65,-428 753.65,-428 1057.05,-428 1057.05,-428"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1057.05,-431.5 1067.05,-428 1057.05,-424.5 1057.05,-431.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1349.56" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
|
||||||
|
</g>
|
||||||
|
<!-- statehistory -->
|
||||||
|
<g id="node21" class="node">
|
||||||
|
<title>statehistory</title>
|
||||||
|
<path fill="#ef9a9a" stroke="black" d="M595.23,-175C595.23,-175 548.77,-175 548.77,-175 542.77,-175 536.77,-169 536.77,-163 536.77,-163 536.77,-151 536.77,-151 536.77,-145 542.77,-139 548.77,-139 548.77,-139 595.23,-139 595.23,-139 601.23,-139 607.23,-145 607.23,-151 607.23,-151 607.23,-163 607.23,-163 607.23,-169 601.23,-175 595.23,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="572" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">StateHistory</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->statehistory -->
|
||||||
|
<g id="edge23" class="edge">
|
||||||
|
<title>servicerequest->statehistory</title>
|
||||||
|
<path fill="none" stroke="black" d="M688.28,-289C640.91,-289 572,-289 572,-289 572,-289 572,-186.87 572,-186.87"/>
|
||||||
|
<polygon fill="black" stroke="black" points="575.5,-186.87 572,-176.87 568.5,-186.87 575.5,-186.87"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="635.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked -->
|
||||||
|
<g id="node22" class="node">
|
||||||
|
<title>vetasked</title>
|
||||||
|
<path fill="#ef9a9a" stroke="black" d="M714.86,-175C714.86,-175 641.14,-175 641.14,-175 635.14,-175 629.14,-169 629.14,-163 629.14,-163 629.14,-151 629.14,-151 629.14,-145 635.14,-139 641.14,-139 641.14,-139 714.86,-139 714.86,-139 720.86,-139 726.86,-145 726.86,-151 726.86,-151 726.86,-163 726.86,-163 726.86,-169 720.86,-175 714.86,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="678" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VeterinarianAsked</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->vetasked -->
|
||||||
|
<g id="edge24" class="edge">
|
||||||
|
<title>servicerequest->vetasked</title>
|
||||||
|
<path fill="none" stroke="black" d="M707.78,-270.3C707.78,-270.3 707.78,-186.66 707.78,-186.66"/>
|
||||||
|
<polygon fill="black" stroke="black" points="711.28,-186.66 707.78,-176.66 704.28,-186.66 711.28,-186.66"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="707.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- payreminder -->
|
||||||
|
<g id="node24" class="node">
|
||||||
|
<title>payreminder</title>
|
||||||
|
<path fill="#e57373" stroke="black" d="M813.29,-175C813.29,-175 760.71,-175 760.71,-175 754.71,-175 748.71,-169 748.71,-163 748.71,-163 748.71,-151 748.71,-151 748.71,-145 754.71,-139 760.71,-139 760.71,-139 813.29,-139 813.29,-139 819.29,-139 825.29,-145 825.29,-151 825.29,-151 825.29,-163 825.29,-163 825.29,-169 819.29,-175 813.29,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="787" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">PayReminder</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->payreminder -->
|
||||||
|
<g id="edge25" class="edge">
|
||||||
|
<title>servicerequest->payreminder</title>
|
||||||
|
<path fill="none" stroke="black" d="M757.57,-270.3C757.57,-270.3 757.57,-186.66 757.57,-186.66"/>
|
||||||
|
<polygon fill="black" stroke="black" points="761.07,-186.66 757.57,-176.66 754.07,-186.66 761.07,-186.66"/>
|
||||||
|
</g>
|
||||||
|
<!-- campaign -->
|
||||||
|
<g id="node38" class="node">
|
||||||
|
<title>campaign</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1196.79,-175C1196.79,-175 1159.21,-175 1159.21,-175 1153.21,-175 1147.21,-169 1147.21,-163 1147.21,-163 1147.21,-151 1147.21,-151 1147.21,-145 1153.21,-139 1159.21,-139 1159.21,-139 1196.79,-139 1196.79,-139 1202.79,-139 1208.79,-145 1208.79,-151 1208.79,-151 1208.79,-163 1208.79,-163 1208.79,-169 1202.79,-175 1196.79,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1178" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Campaign</text>
|
||||||
|
</g>
|
||||||
|
<!-- servicerequest->campaign -->
|
||||||
|
<!-- vetasked->veterinarian -->
|
||||||
|
<g id="edge37" class="edge">
|
||||||
|
<title>vetasked->veterinarian</title>
|
||||||
|
<path fill="none" stroke="black" d="M658.92,-175.35C658.92,-234.76 658.92,-418 658.92,-418 658.92,-418 524.83,-418 524.83,-418"/>
|
||||||
|
<polygon fill="black" stroke="black" points="524.83,-414.5 514.83,-418 524.83,-421.5 524.83,-414.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- vetreminder -->
|
||||||
|
<g id="node23" class="node">
|
||||||
|
<title>vetreminder</title>
|
||||||
|
<path fill="#e57373" stroke="black" d="M703.18,-52C703.18,-52 652.82,-52 652.82,-52 646.82,-52 640.82,-46 640.82,-40 640.82,-40 640.82,-28 640.82,-28 640.82,-22 646.82,-16 652.82,-16 652.82,-16 703.18,-16 703.18,-16 709.18,-16 715.18,-22 715.18,-28 715.18,-28 715.18,-40 715.18,-40 715.18,-46 709.18,-52 703.18,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="678" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00">VetReminder</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetasked->vetreminder -->
|
||||||
|
<g id="edge26" class="edge">
|
||||||
|
<title>vetasked->vetreminder</title>
|
||||||
|
<path fill="none" stroke="black" d="M678,-138.59C678,-138.59 678,-63.65 678,-63.65"/>
|
||||||
|
<polygon fill="black" stroke="black" points="681.5,-63.65 678,-53.65 674.5,-63.65 681.5,-63.65"/>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->servicerequest -->
|
||||||
|
<g id="edge38" class="edge">
|
||||||
|
<title>vetvisit->servicerequest</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2042.05,-270.38C2042.05,-253.18 2042.05,-230 2042.05,-230 2042.05,-230 766.43,-230 766.43,-230 766.43,-230 766.43,-258.87 766.43,-258.87"/>
|
||||||
|
<polygon fill="black" stroke="black" points="762.93,-258.87 766.43,-268.87 769.93,-258.87 762.93,-258.87"/>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport -->
|
||||||
|
<g id="node26" class="node">
|
||||||
|
<title>vetvisitreport</title>
|
||||||
|
<path fill="#fff59d" stroke="black" d="M1932.96,-175C1932.96,-175 1877.04,-175 1877.04,-175 1871.04,-175 1865.04,-169 1865.04,-163 1865.04,-163 1865.04,-151 1865.04,-151 1865.04,-145 1871.04,-139 1877.04,-139 1877.04,-139 1932.96,-139 1932.96,-139 1938.96,-139 1944.96,-145 1944.96,-151 1944.96,-151 1944.96,-163 1944.96,-163 1944.96,-169 1938.96,-175 1932.96,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1905" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitReport</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->vetvisitreport -->
|
||||||
|
<g id="edge28" class="edge">
|
||||||
|
<title>vetvisit->vetvisitreport</title>
|
||||||
|
<path fill="none" stroke="black" d="M2038.69,-289C1992.75,-289 1905,-289 1905,-289 1905,-289 1905,-186.87 1905,-186.87"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1908.5,-186.87 1905,-176.87 1901.5,-186.87 1908.5,-186.87"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1981.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitpetreason -->
|
||||||
|
<g id="node28" class="node">
|
||||||
|
<title>vetvisitpetreason</title>
|
||||||
|
<path fill="#fff176" stroke="black" d="M2039.18,-175C2039.18,-175 1978.82,-175 1978.82,-175 1972.82,-175 1966.82,-169 1966.82,-163 1966.82,-163 1966.82,-151 1966.82,-151 1966.82,-145 1972.82,-139 1978.82,-139 1978.82,-139 2039.18,-139 2039.18,-139 2045.18,-139 2051.18,-145 2051.18,-151 2051.18,-151 2051.18,-163 2051.18,-163 2051.18,-169 2045.18,-175 2039.18,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2009" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VisitPetReason</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->vetvisitpetreason -->
|
||||||
|
<g id="edge29" class="edge">
|
||||||
|
<title>vetvisit->vetvisitpetreason</title>
|
||||||
|
<path fill="none" stroke="black" d="M2048.14,-270.3C2048.14,-270.3 2048.14,-186.66 2048.14,-186.66"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2051.64,-186.66 2048.14,-176.66 2044.64,-186.66 2051.64,-186.66"/>
|
||||||
|
</g>
|
||||||
|
<!-- visit_pets -->
|
||||||
|
<g id="node29" class="node">
|
||||||
|
<title>visit_pets</title>
|
||||||
|
<path fill="#ffee58" stroke="black" d="M2111.72,-170.9C2111.72,-170.9 2084.76,-161.1 2084.76,-161.1 2079.12,-159.05 2079.12,-154.95 2084.76,-152.9 2084.76,-152.9 2111.72,-143.1 2111.72,-143.1 2117.36,-141.05 2128.64,-141.05 2134.28,-143.1 2134.28,-143.1 2161.24,-152.9 2161.24,-152.9 2166.88,-154.95 2166.88,-159.05 2161.24,-161.1 2161.24,-161.1 2134.28,-170.9 2134.28,-170.9 2128.64,-172.95 2117.36,-172.95 2111.72,-170.9"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2123" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Visit-Pets</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->visit_pets -->
|
||||||
|
<g id="edge27" class="edge">
|
||||||
|
<title>vetvisit->visit_pets</title>
|
||||||
|
<path fill="none" stroke="black" d="M2083.24,-270.3C2083.24,-270.3 2083.24,-172.32 2083.24,-172.32"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2086.74,-172.32 2083.24,-162.32 2079.74,-172.32 2086.74,-172.32"/>
|
||||||
|
</g>
|
||||||
|
<!-- receipt -->
|
||||||
|
<g id="node30" class="node">
|
||||||
|
<title>receipt</title>
|
||||||
|
<path fill="#f8bbd9" stroke="black" d="M2237,-175C2237,-175 2207,-175 2207,-175 2201,-175 2195,-169 2195,-163 2195,-163 2195,-151 2195,-151 2195,-145 2201,-139 2207,-139 2207,-139 2237,-139 2237,-139 2243,-139 2249,-145 2249,-151 2249,-151 2249,-163 2249,-163 2249,-169 2243,-175 2237,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2222" y="-160" font-family="Helvetica,sans-Serif" font-size="10.00">Receipt</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="2222" y="-148" font-family="Helvetica,sans-Serif" font-size="10.00">(AFIP)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->receipt -->
|
||||||
|
<g id="edge31" class="edge">
|
||||||
|
<title>vetvisit->receipt</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2093.17,-289C2137.87,-289 2222,-289 2222,-289 2222,-289 2222,-186.87 2222,-186.87"/>
|
||||||
|
<polygon fill="black" stroke="black" points="2225.5,-186.87 2222,-176.87 2218.5,-186.87 2225.5,-186.87"/>
|
||||||
|
</g>
|
||||||
|
<!-- turnfeegroup -->
|
||||||
|
<g id="node41" class="node">
|
||||||
|
<title>turnfeegroup</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1621.79,-175C1621.79,-175 1564.21,-175 1564.21,-175 1558.21,-175 1552.21,-169 1552.21,-163 1552.21,-163 1552.21,-151 1552.21,-151 1552.21,-145 1558.21,-139 1564.21,-139 1564.21,-139 1621.79,-139 1621.79,-139 1627.79,-139 1633.79,-145 1633.79,-151 1633.79,-151 1633.79,-163 1633.79,-163 1633.79,-169 1627.79,-175 1621.79,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1593" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">TurnFeeGroup</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisit->turnfeegroup -->
|
||||||
|
<!-- vetvisitfollowup -->
|
||||||
|
<g id="node27" class="node">
|
||||||
|
<title>vetvisitfollowup</title>
|
||||||
|
<path fill="#fff176" stroke="black" d="M1921.84,-52C1921.84,-52 1888.16,-52 1888.16,-52 1882.16,-52 1876.16,-46 1876.16,-40 1876.16,-40 1876.16,-28 1876.16,-28 1876.16,-22 1882.16,-16 1888.16,-16 1888.16,-16 1921.84,-16 1921.84,-16 1927.84,-16 1933.84,-22 1933.84,-28 1933.84,-28 1933.84,-40 1933.84,-40 1933.84,-46 1927.84,-52 1921.84,-52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1905" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00">FollowUp</text>
|
||||||
|
</g>
|
||||||
|
<!-- vetvisitreport->vetvisitfollowup -->
|
||||||
|
<g id="edge30" class="edge">
|
||||||
|
<title>vetvisitreport->vetvisitfollowup</title>
|
||||||
|
<path fill="none" stroke="black" d="M1905,-138.59C1905,-138.59 1905,-63.65 1905,-63.65"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1908.5,-63.65 1905,-53.65 1901.5,-63.65 1908.5,-63.65"/>
|
||||||
|
</g>
|
||||||
|
<!-- specialty->study -->
|
||||||
|
<!-- province -->
|
||||||
|
<g id="node33" class="node">
|
||||||
|
<title>province</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1366.45,-306.6C1366.45,-306.6 1335.55,-306.6 1335.55,-306.6 1329.55,-306.6 1323.55,-300.6 1323.55,-294.6 1323.55,-294.6 1323.55,-282.6 1323.55,-282.6 1323.55,-276.6 1329.55,-270.6 1335.55,-270.6 1335.55,-270.6 1366.45,-270.6 1366.45,-270.6 1372.45,-270.6 1378.45,-276.6 1378.45,-282.6 1378.45,-282.6 1378.45,-294.6 1378.45,-294.6 1378.45,-300.6 1372.45,-306.6 1366.45,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1351" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Province</text>
|
||||||
|
</g>
|
||||||
|
<!-- locality -->
|
||||||
|
<g id="node34" class="node">
|
||||||
|
<title>locality</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1464,-306.6C1464,-306.6 1434,-306.6 1434,-306.6 1428,-306.6 1422,-300.6 1422,-294.6 1422,-294.6 1422,-282.6 1422,-282.6 1422,-276.6 1428,-270.6 1434,-270.6 1434,-270.6 1464,-270.6 1464,-270.6 1470,-270.6 1476,-276.6 1476,-282.6 1476,-282.6 1476,-294.6 1476,-294.6 1476,-300.6 1470,-306.6 1464,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1449" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Locality</text>
|
||||||
|
</g>
|
||||||
|
<!-- province->locality -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>province->locality</title>
|
||||||
|
<path fill="none" stroke="black" d="M1378.82,-289C1378.82,-289 1410.27,-289 1410.27,-289"/>
|
||||||
|
<polygon fill="black" stroke="black" points="1410.27,-292.5 1420.27,-289 1410.27,-285.5 1410.27,-292.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- petbreed -->
|
||||||
|
<g id="node35" class="node">
|
||||||
|
<title>petbreed</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1543.85,-306.6C1543.85,-306.6 1510.15,-306.6 1510.15,-306.6 1504.15,-306.6 1498.15,-300.6 1498.15,-294.6 1498.15,-294.6 1498.15,-282.6 1498.15,-282.6 1498.15,-276.6 1504.15,-270.6 1510.15,-270.6 1510.15,-270.6 1543.85,-270.6 1543.85,-270.6 1549.85,-270.6 1555.85,-276.6 1555.85,-282.6 1555.85,-282.6 1555.85,-294.6 1555.85,-294.6 1555.85,-300.6 1549.85,-306.6 1543.85,-306.6"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1527" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetBreed</text>
|
||||||
|
</g>
|
||||||
|
<!-- tag -->
|
||||||
|
<g id="node39" class="node">
|
||||||
|
<title>tag</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1432,-175C1432,-175 1402,-175 1402,-175 1396,-175 1390,-169 1390,-163 1390,-163 1390,-151 1390,-151 1390,-145 1396,-139 1402,-139 1402,-139 1432,-139 1432,-139 1438,-139 1444,-145 1444,-151 1444,-151 1444,-163 1444,-163 1444,-169 1438,-175 1432,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1417" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Tag</text>
|
||||||
|
</g>
|
||||||
|
<!-- medication -->
|
||||||
|
<g id="node40" class="node">
|
||||||
|
<title>medication</title>
|
||||||
|
<path fill="#cfd8dc" stroke="black" d="M1518.18,-175C1518.18,-175 1477.82,-175 1477.82,-175 1471.82,-175 1465.82,-169 1465.82,-163 1465.82,-163 1465.82,-151 1465.82,-151 1465.82,-145 1471.82,-139 1477.82,-139 1477.82,-139 1518.18,-139 1518.18,-139 1524.18,-139 1530.18,-145 1530.18,-151 1530.18,-151 1530.18,-163 1530.18,-163 1530.18,-169 1524.18,-175 1518.18,-175"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1498" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Medication</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 43 KiB |
120
cfg/amar/soleprint/atlas/books/arch/graph.html
Normal file
120
cfg/amar/soleprint/atlas/books/arch/graph.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Graph Viewer - AMAR Mascotas</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body class="graph-viewer">
|
||||||
|
<header class="graph-header">
|
||||||
|
<a href="index.html" class="back-link">← Index</a>
|
||||||
|
<div class="nav-controls">
|
||||||
|
<button onclick="navigate(-1)" id="btn-prev" title="Previous (←)">◀</button>
|
||||||
|
<span id="nav-position">1 / 4</span>
|
||||||
|
<button onclick="navigate(1)" id="btn-next" title="Next (→)">▶</button>
|
||||||
|
</div>
|
||||||
|
<h1 id="graph-title">Loading...</h1>
|
||||||
|
<div class="graph-controls">
|
||||||
|
<button onclick="setMode('fit')">Fit</button>
|
||||||
|
<button onclick="setMode('fit-width')">Width</button>
|
||||||
|
<button onclick="setMode('fit-height')">Height</button>
|
||||||
|
<button onclick="setMode('actual-size')">100%</button>
|
||||||
|
<button onclick="downloadSvg()">↓ SVG</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="graph-container" id="graph-container">
|
||||||
|
<img id="graph-img" src="" alt="Graph">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const graphOrder = [
|
||||||
|
'04-data-model-simple',
|
||||||
|
'01-backend-architecture',
|
||||||
|
'02-frontend-architecture',
|
||||||
|
'03-data-model'
|
||||||
|
];
|
||||||
|
|
||||||
|
const graphs = {
|
||||||
|
'04-data-model-simple': {
|
||||||
|
title: 'Data Model Overview',
|
||||||
|
file: '04-data-model-simple.svg'
|
||||||
|
},
|
||||||
|
'01-backend-architecture': {
|
||||||
|
title: 'Backend Architecture',
|
||||||
|
file: '01-backend-architecture.svg'
|
||||||
|
},
|
||||||
|
'02-frontend-architecture': {
|
||||||
|
title: 'Frontend Architecture',
|
||||||
|
file: '02-frontend-architecture.svg'
|
||||||
|
},
|
||||||
|
'03-data-model': {
|
||||||
|
title: 'Detailed Data Model',
|
||||||
|
file: '03-data-model.svg'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
let graphKey = params.get('g') || '04-data-model-simple';
|
||||||
|
let currentIndex = graphOrder.indexOf(graphKey);
|
||||||
|
if (currentIndex === -1) currentIndex = 0;
|
||||||
|
|
||||||
|
function loadGraph(key) {
|
||||||
|
const graph = graphs[key];
|
||||||
|
document.getElementById('graph-title').textContent = graph.title;
|
||||||
|
document.getElementById('graph-img').src = graph.file;
|
||||||
|
document.title = graph.title + ' - AMAR Mascotas';
|
||||||
|
history.replaceState(null, '', '?g=' + key);
|
||||||
|
graphKey = key;
|
||||||
|
updateNavHints();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNavHints() {
|
||||||
|
const idx = graphOrder.indexOf(graphKey);
|
||||||
|
const prevBtn = document.getElementById('btn-prev');
|
||||||
|
const nextBtn = document.getElementById('btn-next');
|
||||||
|
prevBtn.disabled = idx === 0;
|
||||||
|
nextBtn.disabled = idx === graphOrder.length - 1;
|
||||||
|
document.getElementById('nav-position').textContent = (idx + 1) + ' / ' + graphOrder.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(direction) {
|
||||||
|
const idx = graphOrder.indexOf(graphKey);
|
||||||
|
const newIdx = idx + direction;
|
||||||
|
if (newIdx >= 0 && newIdx < graphOrder.length) {
|
||||||
|
currentIndex = newIdx;
|
||||||
|
loadGraph(graphOrder[newIdx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(mode) {
|
||||||
|
const container = document.getElementById('graph-container');
|
||||||
|
container.className = 'graph-container ' + mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSvg() {
|
||||||
|
const graph = graphs[graphKey];
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = graph.file;
|
||||||
|
link.download = graph.file;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
navigate(-1);
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
navigate(1);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
loadGraph(graphOrder[currentIndex]);
|
||||||
|
setMode('fit');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
245
cfg/amar/soleprint/atlas/books/arch/index.html
Normal file
245
cfg/amar/soleprint/atlas/books/arch/index.html
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AMAR Mascotas - Architecture & Data Models</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>AMAR Mascotas</h1>
|
||||||
|
<p class="subtitle">Architecture & Data Model Documentation</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Graph Sections -->
|
||||||
|
<section class="graph-section" id="data-model-simple">
|
||||||
|
<div class="graph-header-row">
|
||||||
|
<h2>Data Model Overview</h2>
|
||||||
|
<a href="graph.html?g=04-data-model-simple" class="view-btn">View Full</a>
|
||||||
|
</div>
|
||||||
|
<a href="graph.html?g=04-data-model-simple" class="graph-preview">
|
||||||
|
<img src="04-data-model-simple.svg" alt="Data Model Overview">
|
||||||
|
</a>
|
||||||
|
<div class="graph-details">
|
||||||
|
<p>High-level entity relationships without field details. Shows the main actors, workflow, and data flow.</p>
|
||||||
|
<h4>Clusters</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Users & Auth</strong>: Django auth.User as central identity</li>
|
||||||
|
<li><strong>Pet Owners & Pets</strong>: Clients, their pets, vaccines, studies</li>
|
||||||
|
<li><strong>Veterinarians</strong>: Vets with availability, specialties, coverage areas</li>
|
||||||
|
<li><strong>Services & Pricing</strong>: Service catalog with dynamic pricing</li>
|
||||||
|
<li><strong>Cart & Checkout</strong>: Shopping cart workflow</li>
|
||||||
|
<li><strong>Service Requests</strong>: Order lifecycle with state machine</li>
|
||||||
|
<li><strong>Veterinary Visits</strong>: Scheduled visits, reports, AFIP invoicing</li>
|
||||||
|
<li><strong>Reference Data</strong>: Lookups (specialties, neighborhoods, vaccines, etc.)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="graph-section" id="backend">
|
||||||
|
<div class="graph-header-row">
|
||||||
|
<h2>Backend Architecture</h2>
|
||||||
|
<a href="graph.html?g=01-backend-architecture" class="view-btn">View Full</a>
|
||||||
|
</div>
|
||||||
|
<a href="graph.html?g=01-backend-architecture" class="graph-preview">
|
||||||
|
<img src="01-backend-architecture.svg" alt="Backend Architecture">
|
||||||
|
</a>
|
||||||
|
<div class="graph-details">
|
||||||
|
<p>Django apps structure: mascotas, productos, solicitudes, common, payments, and external integrations.</p>
|
||||||
|
|
||||||
|
<h4>Celery Tasks</h4>
|
||||||
|
<table class="details-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Task</th><th>App</th><th>Purpose</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>send_veterinarian_followup</code></td>
|
||||||
|
<td>solicitudes</td>
|
||||||
|
<td>Re-sends availability request to vet if still pending</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>run_payment_reminder_cron</code></td>
|
||||||
|
<td>solicitudes</td>
|
||||||
|
<td>Cron job to send payment reminders</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>create_vetvisit_in_sheet</code></td>
|
||||||
|
<td>mascotas</td>
|
||||||
|
<td>Creates row in Google Sheets for visit</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>update_vetvisit_in_sheet</code></td>
|
||||||
|
<td>mascotas</td>
|
||||||
|
<td>Updates Google Sheets row</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>create_event_calendar_vetvisit</code></td>
|
||||||
|
<td>mascotas</td>
|
||||||
|
<td>Creates Google Calendar event</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>update_event_calendar_vetvisit</code></td>
|
||||||
|
<td>mascotas</td>
|
||||||
|
<td>Updates Google Calendar event</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>create_user_owner</code></td>
|
||||||
|
<td>mascotas</td>
|
||||||
|
<td>Creates Django user for PetOwner + welcome email</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>generate_vetvisit_invoice</code></td>
|
||||||
|
<td>mascotas</td>
|
||||||
|
<td>Generates AFIP invoice and PDF</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>fetch_mp_notification_details</code></td>
|
||||||
|
<td>payments</td>
|
||||||
|
<td>Fetches MercadoPago webhook details</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="note">Celery handles async operations: external APIs (Google, MercadoPago, AFIP), scheduled reminders, and heavy processing (invoices, emails).</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="graph-section" id="frontend">
|
||||||
|
<div class="graph-header-row">
|
||||||
|
<h2>Frontend Architecture</h2>
|
||||||
|
<a href="graph.html?g=02-frontend-architecture" class="view-btn">View Full</a>
|
||||||
|
</div>
|
||||||
|
<a href="graph.html?g=02-frontend-architecture" class="graph-preview">
|
||||||
|
<img src="02-frontend-architecture.svg" alt="Frontend Architecture">
|
||||||
|
</a>
|
||||||
|
<div class="graph-details">
|
||||||
|
<p>Next.js 13+ App Router structure with public pages, backoffice, shared components, and services layer.</p>
|
||||||
|
<h4>Key Areas</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Public Pages</strong>: Landing, service catalog, booking flow</li>
|
||||||
|
<li><strong>Backoffice</strong>: Role-based dashboards (admin, vet, petowner)</li>
|
||||||
|
<li><strong>Services Layer</strong>: API clients for backend communication</li>
|
||||||
|
<li><strong>State Management</strong>: Redux store for cart, auth, UI state</li>
|
||||||
|
<li><strong>Shared Components</strong>: Forms, tables, modals, navigation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="graph-section" id="data-model-detailed">
|
||||||
|
<div class="graph-header-row">
|
||||||
|
<h2>Detailed Data Model</h2>
|
||||||
|
<a href="graph.html?g=03-data-model" class="view-btn">View Full</a>
|
||||||
|
</div>
|
||||||
|
<a href="graph.html?g=03-data-model" class="graph-preview">
|
||||||
|
<img src="03-data-model.svg" alt="Detailed Data Model">
|
||||||
|
</a>
|
||||||
|
<div class="graph-details">
|
||||||
|
<p>Complete entity-relationship diagram with all fields, types, and relationships.</p>
|
||||||
|
<h4>Data Patterns</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Soft delete</strong>: <code>deleted</code> flag on most models</li>
|
||||||
|
<li><strong>Audit trail</strong>: <code>StateHistory</code> for service requests</li>
|
||||||
|
<li><strong>Geographic</strong>: PostGIS polygons for coverage areas</li>
|
||||||
|
<li><strong>Versioned pricing</strong>: Date ranges on <code>Prices</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Findings Section -->
|
||||||
|
<section class="findings-section">
|
||||||
|
<h2>Key Findings</h2>
|
||||||
|
|
||||||
|
<div class="findings-grid">
|
||||||
|
<article class="finding-card">
|
||||||
|
<h3>User Types</h3>
|
||||||
|
<p>All users connect to <code>auth.User</code>:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>PetOwner</strong>: Optional 1:1 link (created lazily)</li>
|
||||||
|
<li><strong>Veterinarian</strong>: Required 1:1 link to User</li>
|
||||||
|
<li><strong>Staff</strong>: Direct Django users with <code>is_staff=True</code></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="finding-card">
|
||||||
|
<h3>Core Workflow</h3>
|
||||||
|
<div class="workflow-diagram">
|
||||||
|
<code>PetOwner → Cart → ServiceRequest → VeterinarianAsked → VetVisit → Report</code>
|
||||||
|
</div>
|
||||||
|
<p><strong>State Machine:</strong></p>
|
||||||
|
<div class="state-flow">
|
||||||
|
pending → vet_asked → vet_accepted → coordinated → payed → Confirmado
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="finding-card">
|
||||||
|
<h3>Pricing Logic</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Base price × <code>neighborhood.distance_coefficient</code></li>
|
||||||
|
<li>Optional vet-specific pricing via <code>Prices.veterinarian_id</code></li>
|
||||||
|
<li>Turn fee surcharge via <code>IndividualTurnFeeGroup</code></li>
|
||||||
|
<li>Time-based discounts via <code>Discounts</code> model</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="finding-card">
|
||||||
|
<h3>External Integrations</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>MercadoPago</strong>: Payment processing</li>
|
||||||
|
<li><strong>Google Calendar</strong>: Visit synchronization</li>
|
||||||
|
<li><strong>Google Sheets</strong>: Visit tracking spreadsheet</li>
|
||||||
|
<li><strong>Mercately</strong>: WhatsApp notifications</li>
|
||||||
|
<li><strong>AFIP</strong>: Argentine tax invoicing</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tech Stack Section -->
|
||||||
|
<section class="tech-section">
|
||||||
|
<h2>Technology Stack</h2>
|
||||||
|
<div class="tech-grid">
|
||||||
|
<div class="tech-column">
|
||||||
|
<h3>Backend</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Django 4.x</li>
|
||||||
|
<li>Django REST Framework</li>
|
||||||
|
<li>PostgreSQL + PostGIS</li>
|
||||||
|
<li>Celery (Redis)</li>
|
||||||
|
<li>JWT Authentication</li>
|
||||||
|
<li>django-afip</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tech-column">
|
||||||
|
<h3>Frontend</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Next.js 13+ (App Router)</li>
|
||||||
|
<li>React 18+</li>
|
||||||
|
<li>TypeScript</li>
|
||||||
|
<li>Redux</li>
|
||||||
|
<li>Axios</li>
|
||||||
|
<li>Tailwind CSS</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tech-column">
|
||||||
|
<h3>Infrastructure</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Docker</li>
|
||||||
|
<li>Nginx</li>
|
||||||
|
<li>AWS S3 (storage)</li>
|
||||||
|
<li>MercadoPago API</li>
|
||||||
|
<li>Google APIs</li>
|
||||||
|
<li>WhatsApp Business</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>AMAR Mascotas Architecture Documentation</p>
|
||||||
|
<p class="date">Generated: <time datetime="2024-12-14">December 2024</time></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
565
cfg/amar/soleprint/atlas/books/arch/styles.css
Normal file
565
cfg/amar/soleprint/atlas/books/arch/styles.css
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
/* Reset and base */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-bg: #0f0f0f;
|
||||||
|
--color-surface: #1a1a1a;
|
||||||
|
--color-surface-hover: #252525;
|
||||||
|
--color-border: #333;
|
||||||
|
--color-text: #e0e0e0;
|
||||||
|
--color-text-muted: #888;
|
||||||
|
--color-accent: #4A90D9;
|
||||||
|
--color-accent-light: #6BA3E0;
|
||||||
|
--radius: 8px;
|
||||||
|
--shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
header {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
header .subtitle {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content */
|
||||||
|
main {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Grid - max 3 columns */
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) and (max-width: 999px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Cards */
|
||||||
|
.card {
|
||||||
|
display: block;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-preview {
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-preview img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Findings Grid */
|
||||||
|
.findings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.findings-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-card h3 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-card p,
|
||||||
|
.finding-card ul {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-card ul {
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-card li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-card code {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 0.1em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-diagram {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-diagram code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-flow {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table td {
|
||||||
|
padding: 0.35rem 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table td:first-child {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table td:last-child {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tech Stack Grid */
|
||||||
|
.tech-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.tech-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-column {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-column h3 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-column ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-column li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Viewer Page */
|
||||||
|
.graph-viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header .back-link {
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header .back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-controls button {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
padding: 0.4rem 0.7rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-controls button:hover:not(:disabled) {
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-controls button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-controls span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
min-width: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls button {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls button:hover {
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container.fit-width img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container.fit-height img {
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container.actual-size img {
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Sections (index page) */
|
||||||
|
.graph-section {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header-row h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn:hover {
|
||||||
|
background: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-section .graph-preview {
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-section .graph-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details h4 {
|
||||||
|
margin: 1.5rem 0 0.75rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details h4:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details li {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-details code {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 0.1em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Details Table */
|
||||||
|
.details-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0.5rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-table code {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
main {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid,
|
||||||
|
.findings-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header h1 {
|
||||||
|
order: -1;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
300
cfg/amar/soleprint/atlas/books/drive-index/index.html
Normal file
300
cfg/amar/soleprint/atlas/books/drive-index/index.html
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Drive Index - Amar Mascotas</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.container { max-width: 900px; margin: 0 auto; }
|
||||||
|
.back { font-size: 0.85rem; margin-bottom: 1rem; }
|
||||||
|
.back a { color: #6366f1; text-decoration: none; }
|
||||||
|
.back a:hover { text-decoration: underline; }
|
||||||
|
header {
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
header h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
||||||
|
header p { opacity: 0.9; font-size: 0.85rem; }
|
||||||
|
.stats { display: flex; gap: 0.75rem; margin-top: 0.75rem; flex-wrap: wrap; }
|
||||||
|
.stat { background: rgba(255,255,255,0.2); padding: 0.35rem 0.75rem; border-radius: 6px; font-size: 0.8rem; }
|
||||||
|
.notice {
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #fcd34d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
.notice strong { display: block; margin-bottom: 0.5rem; }
|
||||||
|
.notice ul { margin: 0.5rem 0 0 1.25rem; }
|
||||||
|
.notice li { margin-bottom: 0.25rem; }
|
||||||
|
|
||||||
|
details {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
summary::-webkit-details-marker { display: none; }
|
||||||
|
summary:hover { background: #f9fafb; border-radius: 8px; }
|
||||||
|
summary .icon {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #6366f1;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
details[open] summary .icon { transform: rotate(90deg); }
|
||||||
|
summary .name { font-weight: 500; font-size: 0.9rem; flex: 1; }
|
||||||
|
summary .count {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.folder-content {
|
||||||
|
padding: 0.5rem 1rem 0.75rem 2rem;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.folder-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.file-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.file-ext {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.file-ext.xlsx { background: #dcfce7; color: #166534; }
|
||||||
|
.file-ext.docx { background: #dbeafe; color: #1e40af; }
|
||||||
|
.file-ext.pdf { background: #fee2e2; color: #991b1b; }
|
||||||
|
.file-ext.png, .file-ext.jpg { background: #fef3c7; color: #92400e; }
|
||||||
|
.file-ext.mov, .file-ext.mp4 { background: #f3e8ff; color: #7c3aed; }
|
||||||
|
.file-ext.pptx { background: #ffedd5; color: #c2410c; }
|
||||||
|
.file-ext.ttf { background: #f1f5f9; color: #475569; }
|
||||||
|
|
||||||
|
footer { text-align: center; padding: 2rem; color: #999; font-size: 0.75rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="back"><a href="/">← Album</a></div>
|
||||||
|
<header>
|
||||||
|
<h1>Drive Index</h1>
|
||||||
|
<p>Amar Mascotas - Company Drive Structure</p>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat">12 folders</span>
|
||||||
|
<span class="stat">~235 files</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="notice">
|
||||||
|
<strong>Preview Mode</strong>
|
||||||
|
This shows the Drive folder structure with placeholder filenames. Real file data will be available once <strong>Google Vein</strong> (Google Drive sync) is implemented in Artery:
|
||||||
|
<ul>
|
||||||
|
<li>Auto-sync folder structure and file metadata</li>
|
||||||
|
<li>Direct links to files (requires Google Workspace login)</li>
|
||||||
|
<li>Live updates when Drive contents change</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">01. Identidad</span><span class="count">3 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Brand assets, logos, typography</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext png">png</span> logo_main.png</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> logo_alt.png</div>
|
||||||
|
<div class="file"><span class="file-ext ttf">ttf</span> brand_font.ttf</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">02. Marketing Contenidos</span><span class="count">8 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Social media content, templates, testimonials</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext docx">docx</span> social_template.docx</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> post_image1.png</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> post_image2.png</div>
|
||||||
|
<div class="file"><span class="file-ext mov">mov</span> event_video.mov</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> testimonials.png</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">03. Marketing Growth</span><span class="count">8 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Paid media campaigns, performance dashboards</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> dashboard_report.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> ad_1080x1080.png</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> ad_1200x628.png</div>
|
||||||
|
<div class="file"><span class="file-ext png">png</span> ad_stories.png</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">05. ATC - Operaciones</span><span class="count">12 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Process manuals, workflows, support docs</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext docx">docx</span> manual_procesos.docx</div>
|
||||||
|
<div class="file"><span class="file-ext jpg">jpg</span> workflow_diagram.jpg</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> contacts_list.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> postal_codes.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> tasks_tracker.xlsx</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">06. Supply</span><span class="count">7 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Vet network, lab partnerships, onboarding</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> lab_prices.pdf</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> vet_onboarding.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> vet_cv_sample.pdf</div>
|
||||||
|
<div class="file"><span class="file-ext mp4">mp4</span> meeting_recording.mp4</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">07. Finanzas</span><span class="count">7 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Invoices, accounting, finance tracking</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> finance_tracker.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> invoice_may.pdf</div>
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> invoice_jun.pdf</div>
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> invoice_jul.pdf</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">08. IT y Producto</span><span class="count">57 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Product specs, feature docs, references</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext docx">docx</span> product_index.docx</div>
|
||||||
|
<div class="file"><span class="file-ext docx">docx</span> feature_specs.docx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> platform_states.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> medications_db.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> events_mapping.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext pptx">pptx</span> benchmark.pptx</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">09. DATA</span><span class="count">2 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Database exports, data backups</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> pets_database.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> pets_backup.xlsx</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">Clientes - ventas</span><span class="count">22 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">CRM exports, sales data, client info</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> crm_export.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> churn_analysis.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> leads_2023.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> leads_2024.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> monthly_report.pdf</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">HR</span><span class="count">17 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Onboarding, job profiles, team docs</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext pptx">pptx</span> onboarding_deck.pptx</div>
|
||||||
|
<div class="file"><span class="file-ext docx">docx</span> job_profile_sales.docx</div>
|
||||||
|
<div class="file"><span class="file-ext pdf">pdf</span> job_profile_growth.pdf</div>
|
||||||
|
<div class="file"><span class="file-ext jpg">jpg</span> team_photo.jpg</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">Pitch Decks</span><span class="count">24 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Investor presentations, company decks</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext pptx">pptx</span> pitch_deck_latest.pptx</div>
|
||||||
|
<div class="file"><span class="file-ext docx">docx</span> video_script.docx</div>
|
||||||
|
<div class="file"><span class="file-ext pptx">pptx</span> product_timeline.pptx</div>
|
||||||
|
<div class="file"><span class="file-ext pptx">pptx</span> investor_update.pptx</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><span class="icon">▶</span><span class="name">z.Backup archivos viejos</span><span class="count">46 files</span></summary>
|
||||||
|
<div class="folder-content">
|
||||||
|
<div class="folder-desc">Legacy files, old exports</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> old_vets_list.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> org_chart.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> data_room_2023.xlsx</div>
|
||||||
|
<div class="file"><span class="file-ext xlsx">xlsx</span> legacy_export.xlsx</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<footer>pawprint/album</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
217
cfg/amar/soleprint/atlas/books/drive-index/index.md
Normal file
217
cfg/amar/soleprint/atlas/books/drive-index/index.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Drive Index - Amar Mascotas
|
||||||
|
|
||||||
|
> vault source: `vault/drive/`
|
||||||
|
> 48 files | Last indexed: 2025-12-08
|
||||||
|
|
||||||
|
## Overview by Area
|
||||||
|
|
||||||
|
| Area | Focus | Key Files | Workflow Potential |
|
||||||
|
|------|-------|-----------|-------------------|
|
||||||
|
| 01. Identidad | Brand assets | Illustrations, Typography | Design handoff |
|
||||||
|
| 02. Marketing Contenidos | Social/Content | Posts, Event media, Templates | Content calendar |
|
||||||
|
| 03. Marketing Growth | Paid/Performance | Ad creatives, Dashboards | Campaign tracking |
|
||||||
|
| 05. ATC - Operaciones | Ops/Support | Process manual, Workflow diagram | Onboarding, SOP |
|
||||||
|
| 06. Supply | Vets/Labs | Vet tracking, Lab prices, CVs | Vet onboarding |
|
||||||
|
| 07. Finanzas | Accounting | Invoices, Finance sheet | Reconciliation |
|
||||||
|
| Clientes | Sales/CRM | Fitsales export | Client tracking |
|
||||||
|
| Pitch Decks | Presentations | Company pitch | Investor/partner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 01. Identidad Amar Mascotas
|
||||||
|
Brand identity assets.
|
||||||
|
|
||||||
|
```
|
||||||
|
Ilustraciones amar PNG/
|
||||||
|
├── Asset 4@4x.png
|
||||||
|
└── Asset 11@4x.png
|
||||||
|
|
||||||
|
Tipografia/Open_Sans/static/
|
||||||
|
└── OpenSans_Condensed-Italic.ttf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**: Design system, brand consistency checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 02. Marketing Contenidos
|
||||||
|
Social media content and production assets.
|
||||||
|
|
||||||
|
```
|
||||||
|
Backup archivos viejos/
|
||||||
|
├── Contenido evento amar 10 ago 2023/
|
||||||
|
│ ├── IMG_7309.MOV
|
||||||
|
│ └── video-output-(...).mov
|
||||||
|
└── RRSS MSJ PREDETERMINADOS.docx ← Predefined social messages
|
||||||
|
|
||||||
|
Produccion audiovisual Jul 24/
|
||||||
|
└── Fotos Produccion Audiovisual JUL24/
|
||||||
|
└── Vacunacion Fotos Produccion JUL 24/
|
||||||
|
└── IMG_4620.JPG
|
||||||
|
|
||||||
|
Redes sociales Amar/
|
||||||
|
├── 02. FEBRERO 25 Finales/12-2/cambios/247.png
|
||||||
|
├── 08. AGOSTO 24 Finales Amar/
|
||||||
|
│ ├── 13.8/1.png
|
||||||
|
│ └── 21.8/3.png
|
||||||
|
└── Comentarios clientes felices JUN-JUL24/
|
||||||
|
└── Captura de pantalla (...).png ← Testimonials
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- `RRSS MSJ PREDETERMINADOS.docx` = template for ATC/social responses
|
||||||
|
- Monthly folders suggest content calendar exists
|
||||||
|
- Testimonials capture = social proof workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 03. Marketing Growth
|
||||||
|
Paid media campaigns and performance tracking.
|
||||||
|
|
||||||
|
```
|
||||||
|
Paid Media - Pauta digital/
|
||||||
|
├── 05.MAY24 - ADS Testimonios/
|
||||||
|
├── 08.AGO24 - ADS 10 OFF/
|
||||||
|
│ ├── 1080 x 1080-17.png
|
||||||
|
│ ├── 1200 x 628-32.png
|
||||||
|
│ ├── 1200 x 628-33.png
|
||||||
|
│ ├── 1200 x 628-38.png
|
||||||
|
│ ├── 1200 x 628-39.png
|
||||||
|
│ └── 960 x 1200-30.png
|
||||||
|
├── Amar Mascotas - Reporte_Dash general(...)(2).xlsx
|
||||||
|
└── Amar Mascotas - Reporte_Dash general(...)(3).xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- Ad size variants = multi-platform campaigns (Meta, Google)
|
||||||
|
- Dashboard reports = performance tracking exists
|
||||||
|
- Monthly campaign folders = structured campaign workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 05. ATC - Operaciones
|
||||||
|
Operations, customer service, and legacy data.
|
||||||
|
|
||||||
|
```
|
||||||
|
Operaciones backup viejos/
|
||||||
|
├── ATC - Manual de Procesos.docx ← KEY: Process documentation
|
||||||
|
├── work Flow servicio veterinario.jpg ← KEY: Service workflow diagram
|
||||||
|
├── Clientes a contactar.xlsx
|
||||||
|
├── codigos_postales_y_barrios (1).xlsx ← Coverage/delivery zones
|
||||||
|
├── domesticables ultimo.docx
|
||||||
|
├── domesticables ultimo.pdf
|
||||||
|
├── INFO.xlsx
|
||||||
|
├── Reporte DOMESTICABLES 11_5.xlsx
|
||||||
|
├── Tareas de Leila.xlsx
|
||||||
|
└── entrega/
|
||||||
|
├── prestashop/
|
||||||
|
│ ├── clientes a escribir febrero 2023.csv
|
||||||
|
│ └── datos web vieja/
|
||||||
|
│ └── reembolsos por falta de stock.csv
|
||||||
|
└── vendedores/somos pam/
|
||||||
|
└── articulos desactivados en marzo 2023.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- `ATC - Manual de Procesos.docx` = existing SOP (Gherkin source?)
|
||||||
|
- `work Flow servicio veterinario.jpg` = visual process (BDD source?)
|
||||||
|
- Legacy CSV = migration/history context
|
||||||
|
- Task tracking = ops workflow exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 06. Supply (vetes-labo-clinicas)
|
||||||
|
Veterinarian network and lab partnerships.
|
||||||
|
|
||||||
|
```
|
||||||
|
Comunciacion/
|
||||||
|
└── Veterinarios-46.jpg
|
||||||
|
|
||||||
|
Formularios- Viejos - no usar/
|
||||||
|
└── Reuniones Matias/
|
||||||
|
├── Grabación de pantalla 2024-08-20(...).mp4
|
||||||
|
└── Grabación de pantalla 2024-08-21(...).mp4
|
||||||
|
|
||||||
|
Laboratorios/DiagnoTest/
|
||||||
|
└── Diagnotest-ListaPrecios-Junio2025 (1).pdf ← Current pricing
|
||||||
|
|
||||||
|
Veterinarios postulantes/
|
||||||
|
├── Aldana Becker - Colaboracion Amar Mascotas.docx
|
||||||
|
├── Ana Christophersen/
|
||||||
|
│ └── CV - Ana Christophersen.pdf
|
||||||
|
└── Ingreso Veterinarios.xlsx ← Vet onboarding tracker
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- `Ingreso Veterinarios.xlsx` = vet recruitment/onboarding flow
|
||||||
|
- Lab price list = service pricing reference
|
||||||
|
- Meeting recordings = training/knowledge transfer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 07. Finanzas y contabilidad
|
||||||
|
Invoices and financial tracking.
|
||||||
|
|
||||||
|
```
|
||||||
|
Facturas/
|
||||||
|
├── May 23/Recibido/
|
||||||
|
│ ├── FacturaAMARMASCOTASSA(...).pdf
|
||||||
|
│ └── FacturaAMARMASCOTASSA(...).pdf
|
||||||
|
├── Jun 23/Recibido/
|
||||||
|
│ ├── nc 4-29.pdf
|
||||||
|
│ └── nc 4-30.pdf
|
||||||
|
└── Jul 23/Recibido/
|
||||||
|
├── FAC 567 ZUCCONI ALAN EDGARDO.pdf
|
||||||
|
└── Mercaba carlos roberto cozak.pdf
|
||||||
|
|
||||||
|
Finanzas Amar mascotas.xlsx ← Main finance tracker
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- Monthly invoice organization = accounting workflow
|
||||||
|
- Finance xlsx = reconciliation process
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clientes - ventas - devoluciones
|
||||||
|
CRM and sales data.
|
||||||
|
|
||||||
|
```
|
||||||
|
Excels Clientes - ventas - devoluciones/
|
||||||
|
└── Fitsales CRM vinculados hasta el 06 dic.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- CRM export = client tracking, sales funnel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitch Decks - Presentaciones
|
||||||
|
Company presentations.
|
||||||
|
|
||||||
|
```
|
||||||
|
Copias Pitch Deck Cata/
|
||||||
|
└── Copia de Pitch Deck - ABR 2025.pptx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow relevance**:
|
||||||
|
- Investor/partner communications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Discovery Summary
|
||||||
|
|
||||||
|
### Documents to Review for BDD/Gherkin
|
||||||
|
1. `05. ATC/ATC - Manual de Procesos.docx` - existing process documentation
|
||||||
|
2. `05. ATC/work Flow servicio veterinario.jpg` - visual workflow
|
||||||
|
3. `02. Marketing/RRSS MSJ PREDETERMINADOS.docx` - response templates
|
||||||
|
|
||||||
|
### Active Tracking Sheets
|
||||||
|
- `06. Supply/Ingreso Veterinarios.xlsx` - vet onboarding
|
||||||
|
- `Clientes/Fitsales CRM vinculados.xlsx` - client tracking
|
||||||
|
- `07. Finanzas/Finanzas Amar mascotas.xlsx` - finance
|
||||||
|
|
||||||
|
### Potential Artery Connections
|
||||||
|
- Fitsales CRM → pulse for client data
|
||||||
|
- Finance xlsx → pulse for accounting
|
||||||
|
- Vet tracker → pulse for supply management
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Ops Templates Sample Book
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
**SAMPLE DATA** - Example filled ops/support templates for demonstration and testing purposes. These are realistic examples to work with while actual definitions are pending from ops/support team.
|
||||||
|
|
||||||
|
Use these to:
|
||||||
|
- Understand the template structure
|
||||||
|
- Test tooling and workflows
|
||||||
|
- Demo the feature-flow pipeline
|
||||||
|
|
||||||
|
Real templates will follow the same structure but with validated content.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ops-templates-sample/
|
||||||
|
├── pet-owner/ # 5 flows for pet owners
|
||||||
|
│ ├── 01-registro.md
|
||||||
|
│ ├── 02-reservar-turno.md
|
||||||
|
│ ├── 03-gestion-mascotas.md
|
||||||
|
│ ├── 04-pago-turno.md
|
||||||
|
│ └── 05-historial-medico.md
|
||||||
|
├── veterinarian/ # 5 flows for vets
|
||||||
|
│ ├── 01-aceptar-solicitud.md
|
||||||
|
│ ├── 02-gestion-agenda.md
|
||||||
|
│ ├── 03-realizar-visita.md
|
||||||
|
│ ├── 04-zonas-cobertura.md
|
||||||
|
│ └── 05-historial-pacientes.md
|
||||||
|
└── backoffice/ # 5 flows for admins
|
||||||
|
├── 01-gestion-solicitudes.md
|
||||||
|
├── 02-gestion-usuarios.md
|
||||||
|
├── 03-gestion-servicios.md
|
||||||
|
├── 04-reembolsos.md
|
||||||
|
└── 05-reportes.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Format
|
||||||
|
|
||||||
|
Each file follows the standard ops template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# [Nombre del Flujo]
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
## Donde empieza
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
## Pasos
|
||||||
|
## Que deberia pasar
|
||||||
|
## Problemas comunes
|
||||||
|
## Casos especiales
|
||||||
|
## Flujos relacionados
|
||||||
|
## Notas tecnicas
|
||||||
|
```
|
||||||
|
|
||||||
|
## Corresponding Gherkin
|
||||||
|
|
||||||
|
Each template has corresponding `.feature` files in:
|
||||||
|
- `album/book/gherkin-sample/es/` - Spanish keywords (Dado/Cuando/Entonces)
|
||||||
|
- `album/book/gherkin-sample/en/` - English keywords (Given/When/Then)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Support/Ops**: Use templates to document new flows
|
||||||
|
2. **Dev**: Convert templates to Gherkin specs
|
||||||
|
3. **QA**: Use Gherkin to derive test cases
|
||||||
|
4. **Product**: Review templates for completeness
|
||||||
|
|
||||||
|
## Source Template
|
||||||
|
|
||||||
|
The base template is at: `album/template/ops-flow/plantilla-flujo.md`
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
| Area | Flows | Status |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Pet Owner | 5 | Complete |
|
||||||
|
| Veterinarian | 5 | Complete |
|
||||||
|
| Backoffice | 5 | Complete |
|
||||||
|
| **Total** | **15** | |
|
||||||
335
cfg/amar/soleprint/atlas/books/feature-form-samples/detail.html
Normal file
335
cfg/amar/soleprint/atlas/books/feature-form-samples/detail.html
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ filename }} - Ops Template</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.container { max-width: 800px; margin: 0 auto; padding: 2rem 1rem; }
|
||||||
|
header { margin-bottom: 1.5rem; }
|
||||||
|
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
|
||||||
|
.breadcrumb a { color: #15803d; text-decoration: none; }
|
||||||
|
.breadcrumb a:hover { text-decoration: underline; }
|
||||||
|
h1 { font-size: 1.5rem; color: #15803d; }
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.meta span {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.meta .sample { background: #fef3c7; color: #92400e; }
|
||||||
|
|
||||||
|
/* Form-like styling */
|
||||||
|
.form-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.form-header {
|
||||||
|
background: linear-gradient(135deg, #15803d, #22c55e);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
.form-header h2 { font-size: 1.1rem; font-weight: 600; }
|
||||||
|
.form-body { padding: 1.5rem; }
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
.field:last-child { margin-bottom: 0; }
|
||||||
|
.field-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
.field-label .edit-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.field-label .edit-icon:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.field-value {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #1e293b;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
}
|
||||||
|
.field-value:focus-within {
|
||||||
|
border-color: #15803d;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.1);
|
||||||
|
}
|
||||||
|
.field-value.multiline {
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
.field-value ul, .field-value ol {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
.field-value li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.field-value p {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
.field-value p:last-child { margin-bottom: 0; }
|
||||||
|
.field-textarea {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
border: 1px solid #15803d;
|
||||||
|
border-radius: 6px;
|
||||||
|
resize: vertical;
|
||||||
|
box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.1);
|
||||||
|
}
|
||||||
|
.field.editing .field-value { display: none; }
|
||||||
|
.field.editing .field-textarea { display: block; }
|
||||||
|
|
||||||
|
/* Special field styles */
|
||||||
|
.field-steps .field-value {
|
||||||
|
counter-reset: step;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.field-steps ol {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.field-steps li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.field-steps li::before {
|
||||||
|
counter-increment: step;
|
||||||
|
content: counter(step);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: #15803d;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-problems .field-value {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
.field-problems li::before {
|
||||||
|
content: '!';
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-special .field-value {
|
||||||
|
background: #fffbeb;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-technical .field-value {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.field-technical code {
|
||||||
|
background: #dcfce7;
|
||||||
|
padding: 0.1rem 0.35rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.sidebar h3 { font-size: 0.8rem; color: #15803d; margin-bottom: 0.75rem; text-transform: uppercase; }
|
||||||
|
.sidebar a {
|
||||||
|
display: block;
|
||||||
|
color: #15803d;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.35rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.sidebar a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
footer a { color: #15803d; text-decoration: none; }
|
||||||
|
footer a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<a href="/">Album</a> / <a href="/book/ops-templates/">Ops Templates</a> / {{ user_type }}
|
||||||
|
</div>
|
||||||
|
<h1>{{ filename.replace('.md', '').replace('-', ' ').title() }}</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>{{ user_type }}</span>
|
||||||
|
<span class="sample">Sample</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="form-card">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2>User Flow Template</h2>
|
||||||
|
</div>
|
||||||
|
<div class="form-body" id="form-content">
|
||||||
|
<!-- Content will be parsed and inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<h3>Related</h3>
|
||||||
|
<a href="/book/gherkin/es/{{ user_type }}/{{ filename.replace('.md', '.feature') }}">Gherkin (Spanish)</a>
|
||||||
|
<a href="/book/gherkin/en/{{ user_type }}/{{ filename.replace('.md', '.feature') }}">Gherkin (English)</a>
|
||||||
|
<a href="/book/feature-flow/">Feature Flow Pipeline</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<a href="/book/ops-templates/">← All Templates</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const rawContent = {{ content | tojson }};
|
||||||
|
|
||||||
|
function parseMarkdown(md) {
|
||||||
|
const sections = {};
|
||||||
|
let currentSection = null;
|
||||||
|
let currentContent = [];
|
||||||
|
|
||||||
|
const lines = md.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('## ')) {
|
||||||
|
if (currentSection) {
|
||||||
|
sections[currentSection] = currentContent.join('\n').trim();
|
||||||
|
}
|
||||||
|
currentSection = line.replace('## ', '').trim();
|
||||||
|
currentContent = [];
|
||||||
|
} else if (line.startsWith('# ')) {
|
||||||
|
sections['_title'] = line.replace('# ', '').trim();
|
||||||
|
} else if (currentSection) {
|
||||||
|
currentContent.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentSection) {
|
||||||
|
sections[currentSection] = currentContent.join('\n').trim();
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatContent(content) {
|
||||||
|
if (!content) return '';
|
||||||
|
|
||||||
|
// Convert markdown lists to HTML
|
||||||
|
let html = content
|
||||||
|
.replace(/^(\d+)\.\s+(.*)$/gm, '<li>$2</li>')
|
||||||
|
.replace(/^-\s+(.*)$/gm, '<li>$1</li>')
|
||||||
|
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// Wrap consecutive li elements in ul/ol
|
||||||
|
if (html.includes('<li>')) {
|
||||||
|
if (content.match(/^\d+\./m)) {
|
||||||
|
html = '<ol>' + html + '</ol>';
|
||||||
|
} else {
|
||||||
|
html = '<ul>' + html + '</ul>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert line breaks to paragraphs for non-list content
|
||||||
|
if (!html.includes('<li>')) {
|
||||||
|
html = html.split('\n').filter(l => l.trim()).map(l => '<p>' + l + '</p>').join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createField(label, content, className = '') {
|
||||||
|
if (!content) return '';
|
||||||
|
return `
|
||||||
|
<div class="field ${className}">
|
||||||
|
<label class="field-label">${label} <span class="edit-icon" onclick="toggleEdit(this)" title="Edit field">✎</span></label>
|
||||||
|
<div class="field-value ${content.includes('\n') ? 'multiline' : ''}">${formatContent(content)}</div>
|
||||||
|
<textarea class="field-textarea">${content}</textarea>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEdit(icon) {
|
||||||
|
const field = icon.closest('.field');
|
||||||
|
field.classList.toggle('editing');
|
||||||
|
if (field.classList.contains('editing')) {
|
||||||
|
field.querySelector('.field-textarea').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = parseMarkdown(rawContent);
|
||||||
|
const container = document.getElementById('form-content');
|
||||||
|
|
||||||
|
const fieldMap = [
|
||||||
|
{ key: 'Tipo de usuario', label: 'Tipo de Usuario', class: '' },
|
||||||
|
{ key: 'Donde empieza', label: 'Punto de Entrada', class: '' },
|
||||||
|
{ key: 'Que quiere hacer el usuario', label: 'Objetivo del Usuario', class: '' },
|
||||||
|
{ key: 'Pasos', label: 'Pasos', class: 'field-steps' },
|
||||||
|
{ key: 'Que deberia pasar', label: 'Resultado Esperado', class: '' },
|
||||||
|
{ key: 'Problemas comunes', label: 'Problemas Comunes', class: 'field-problems' },
|
||||||
|
{ key: 'Casos especiales', label: 'Casos Especiales', class: 'field-special' },
|
||||||
|
{ key: 'Flujos relacionados', label: 'Flujos Relacionados', class: '' },
|
||||||
|
{ key: 'Notas tecnicas', label: 'Notas Tecnicas', class: 'field-technical' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const field of fieldMap) {
|
||||||
|
if (sections[field.key]) {
|
||||||
|
html += createField(field.label, sections[field.key], field.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Gestion de Solicitudes de Servicio
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Administrador / Operaciones
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Backoffice `/admin/` -> Dashboard o Seccion "Solicitudes"
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Ver, filtrar y gestionar todas las solicitudes de servicio del sistema
|
||||||
|
|
||||||
|
## Pasos - Ver solicitudes
|
||||||
|
|
||||||
|
1. Acceder al backoffice
|
||||||
|
2. Ir a seccion "Solicitudes"
|
||||||
|
3. Ver listado con todas las solicitudes
|
||||||
|
4. Usar filtros para encontrar solicitudes especificas:
|
||||||
|
- Por estado (Pendiente, Coordinado, Pagado, Completado, Cancelado)
|
||||||
|
- Por fecha
|
||||||
|
- Por barrio/zona
|
||||||
|
- Por veterinario
|
||||||
|
- Por dueno
|
||||||
|
|
||||||
|
## Pasos - Ver detalle de solicitud
|
||||||
|
|
||||||
|
1. Click en una solicitud del listado
|
||||||
|
2. Ver toda la informacion:
|
||||||
|
- Datos del dueno
|
||||||
|
- Datos de la mascota
|
||||||
|
- Servicios solicitados
|
||||||
|
- Fechas preferidas
|
||||||
|
- Veterinario asignado (si hay)
|
||||||
|
- Estado de pago
|
||||||
|
- Historial de cambios de estado
|
||||||
|
|
||||||
|
## Pasos - Asignar veterinario manualmente
|
||||||
|
|
||||||
|
1. Abrir solicitud en estado "Pendiente"
|
||||||
|
2. Click en "Asignar veterinario"
|
||||||
|
3. Ver lista de vets disponibles en la zona
|
||||||
|
4. Seleccionar veterinario
|
||||||
|
5. Seleccionar fecha y hora
|
||||||
|
6. Confirmar asignacion
|
||||||
|
7. Sistema notifica al vet y al dueno
|
||||||
|
|
||||||
|
## Pasos - Cambiar estado manualmente
|
||||||
|
|
||||||
|
1. Abrir solicitud
|
||||||
|
2. Click en "Cambiar estado"
|
||||||
|
3. Seleccionar nuevo estado
|
||||||
|
4. Ingresar motivo (si aplica)
|
||||||
|
5. Confirmar
|
||||||
|
6. Se registra en el historial
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Vision completa de todas las solicitudes
|
||||||
|
- Capacidad de intervenir cuando algo falla
|
||||||
|
- Trazabilidad de cambios
|
||||||
|
- Notificaciones a las partes afectadas
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- Solicitud sin vet disponible en la zona
|
||||||
|
- Usuario pago pero webhook fallo
|
||||||
|
- Vet no puede asistir y hay que reasignar
|
||||||
|
- Dueno quiere cancelar pero ya pago
|
||||||
|
- Solicitudes duplicadas del mismo dueno
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Solicitud urgente sin vets disponibles
|
||||||
|
- Reasignacion a ultimo momento
|
||||||
|
- Cancelacion con reembolso
|
||||||
|
- Solicitud con datos incompletos
|
||||||
|
- Dueno no responde a confirmaciones
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Proceso de reembolso
|
||||||
|
- Gestion de usuarios
|
||||||
|
- Reportes
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- APIs:
|
||||||
|
- `GET /solicitudes/api/v1/service-requests/` (listado con filtros)
|
||||||
|
- `GET /solicitudes/api/v1/service-requests/{id}/` (detalle)
|
||||||
|
- `PATCH /solicitudes/api/v1/service-requests/{id}/` (asignar vet)
|
||||||
|
- `PATCH /solicitudes/api/v1/change-service-request-state/` (cambiar estado)
|
||||||
|
- Estados validos: pending, coordinated, payed, completed, cancelled
|
||||||
|
- Permisos: solo staff puede ver todas las solicitudes
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Gestion de Usuarios
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Administrador / Operaciones
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Backoffice `/admin/` -> Seccion "Usuarios" o "Duenos" / "Veterinarios"
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Administrar duenos de mascotas y veterinarios del sistema
|
||||||
|
|
||||||
|
## Pasos - Ver/Buscar duenos
|
||||||
|
|
||||||
|
1. Ir a "Duenos de mascotas"
|
||||||
|
2. Ver listado de todos los duenos
|
||||||
|
3. Usar buscador (nombre, email, telefono)
|
||||||
|
4. Filtrar por:
|
||||||
|
- Tipo (Registrado / Invitado)
|
||||||
|
- Fecha de registro
|
||||||
|
- Con/sin turnos activos
|
||||||
|
|
||||||
|
## Pasos - Ver detalle de dueno
|
||||||
|
|
||||||
|
1. Click en un dueno
|
||||||
|
2. Ver informacion completa:
|
||||||
|
- Datos personales
|
||||||
|
- Direcciones registradas
|
||||||
|
- Mascotas
|
||||||
|
- Historial de solicitudes
|
||||||
|
- Historial de pagos
|
||||||
|
|
||||||
|
## Pasos - Editar dueno
|
||||||
|
|
||||||
|
1. En el detalle, click "Editar"
|
||||||
|
2. Modificar campos permitidos:
|
||||||
|
- Nombre
|
||||||
|
- Telefono
|
||||||
|
- Email
|
||||||
|
- Direccion
|
||||||
|
3. Guardar cambios
|
||||||
|
|
||||||
|
## Pasos - Gestionar veterinarios
|
||||||
|
|
||||||
|
1. Ir a "Veterinarios"
|
||||||
|
2. Ver listado con:
|
||||||
|
- Nombre y matricula
|
||||||
|
- Zonas de cobertura
|
||||||
|
- Estado (Activo/Inactivo)
|
||||||
|
- Turnos del mes
|
||||||
|
|
||||||
|
## Pasos - Agregar nuevo veterinario
|
||||||
|
|
||||||
|
1. Click en "Agregar veterinario"
|
||||||
|
2. Completar datos:
|
||||||
|
- Nombre completo
|
||||||
|
- Numero de matricula
|
||||||
|
- Email
|
||||||
|
- Telefono
|
||||||
|
- Especialidades
|
||||||
|
- Zonas de cobertura inicial
|
||||||
|
3. Crear credenciales de acceso
|
||||||
|
4. Guardar
|
||||||
|
|
||||||
|
## Pasos - Desactivar veterinario
|
||||||
|
|
||||||
|
1. Abrir perfil del vet
|
||||||
|
2. Click en "Desactivar"
|
||||||
|
3. Confirmar
|
||||||
|
4. Vet deja de recibir solicitudes
|
||||||
|
5. Visitas ya aceptadas se mantienen o se reasignan
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Gestion completa de usuarios del sistema
|
||||||
|
- Capacidad de corregir datos erroneos
|
||||||
|
- Alta/baja de veterinarios
|
||||||
|
- Vision del estado de cada usuario
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- Duenos duplicados (se registraron con email diferente)
|
||||||
|
- Vet quiere darse de baja pero tiene citas pendientes
|
||||||
|
- Email incorrecto y no reciben notificaciones
|
||||||
|
- Cuenta de invitado que quiere convertirse en registrada
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Fusionar cuentas duplicadas
|
||||||
|
- Transferir mascota a otro dueno
|
||||||
|
- Vet de vacaciones temporalmente
|
||||||
|
- Cambio de numero de matricula
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Gestion de solicitudes (ver historial por usuario)
|
||||||
|
- Reportes (filtrar por usuario)
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- APIs:
|
||||||
|
- `GET /mascotas/api/v1/pet-owners/` (duenos)
|
||||||
|
- `GET /mascotas/api/v1/veterinarians/` (vets)
|
||||||
|
- `POST /mascotas/api/v1/veterinarians/` (crear vet)
|
||||||
|
- `PATCH /mascotas/api/v1/veterinarians/{id}/` (editar/desactivar)
|
||||||
|
- Permisos: solo staff puede modificar usuarios
|
||||||
|
- Soft delete para mantener historial
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Gestion de Servicios y Precios
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Administrador
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Backoffice `/admin/` -> Seccion "Servicios" o Django Admin -> Productos
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Administrar el catalogo de servicios veterinarios y sus precios
|
||||||
|
|
||||||
|
## Pasos - Ver servicios
|
||||||
|
|
||||||
|
1. Ir a "Servicios"
|
||||||
|
2. Ver listado de todos los servicios
|
||||||
|
3. Filtrar por:
|
||||||
|
- Categoria (Vacunacion, Consulta, Cirugia, etc.)
|
||||||
|
- Tipo de mascota (Perro, Gato, Ambos)
|
||||||
|
- Estado (Activo/Inactivo)
|
||||||
|
|
||||||
|
## Pasos - Agregar nuevo servicio
|
||||||
|
|
||||||
|
1. Click en "Agregar servicio"
|
||||||
|
2. Completar datos:
|
||||||
|
- Nombre del servicio
|
||||||
|
- Descripcion
|
||||||
|
- Categoria
|
||||||
|
- Tipo de mascota aplicable
|
||||||
|
- Precio base
|
||||||
|
- Duracion estimada
|
||||||
|
- Requiere otros servicios (dependencias)
|
||||||
|
3. Guardar
|
||||||
|
|
||||||
|
## Pasos - Editar servicio
|
||||||
|
|
||||||
|
1. Abrir servicio existente
|
||||||
|
2. Modificar campos
|
||||||
|
3. Guardar
|
||||||
|
4. Cambios aplican a nuevas solicitudes
|
||||||
|
|
||||||
|
## Pasos - Gestionar precios
|
||||||
|
|
||||||
|
1. Abrir servicio
|
||||||
|
2. Ir a seccion "Precios"
|
||||||
|
3. Ver precio actual
|
||||||
|
4. Para cambiar precio:
|
||||||
|
- Click "Agregar precio"
|
||||||
|
- Ingresar monto
|
||||||
|
- Definir fecha de vigencia
|
||||||
|
- Guardar
|
||||||
|
5. Precio anterior queda en historial
|
||||||
|
|
||||||
|
## Pasos - Configurar combos
|
||||||
|
|
||||||
|
1. Ir a "Combos" o "Paquetes"
|
||||||
|
2. Crear nuevo combo:
|
||||||
|
- Nombre del combo
|
||||||
|
- Servicios incluidos
|
||||||
|
- Precio del combo (con descuento)
|
||||||
|
- Condiciones de aplicacion
|
||||||
|
3. Guardar
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Catalogo actualizado de servicios
|
||||||
|
- Precios correctos en el turnero
|
||||||
|
- Combos aplicados automaticamente
|
||||||
|
- Historial de precios para facturacion
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- Cambio de precio afecta solicitudes ya creadas
|
||||||
|
- Servicio deprecado pero con solicitudes pendientes
|
||||||
|
- Combo no se aplica automaticamente
|
||||||
|
- Precio diferente por zona/vet
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Servicio con precio variable (depende de peso mascota)
|
||||||
|
- Servicio estacional (solo ciertas epocas)
|
||||||
|
- Promocion temporal
|
||||||
|
- Precio especial por vet
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Turnero (muestra servicios disponibles)
|
||||||
|
- Facturacion (usa precios)
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- APIs:
|
||||||
|
- `GET /productos/api/v1/services/` (servicios)
|
||||||
|
- `POST /productos/api/v1/services/` (crear)
|
||||||
|
- `PATCH /productos/api/v1/services/{id}/` (editar)
|
||||||
|
- `GET /productos/api/v1/prices/` (precios)
|
||||||
|
- `POST /productos/api/v1/prices/` (nuevo precio)
|
||||||
|
- Modelo Price tiene fecha de vigencia
|
||||||
|
- Precio se congela al momento de crear solicitud
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Proceso de Reembolso
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Administrador / Operaciones
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Backoffice -> Solicitud pagada -> "Procesar reembolso" o Seccion "Pagos"
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Procesar un reembolso para un turno que fue cancelado despues del pago
|
||||||
|
|
||||||
|
## Pasos - Identificar solicitud a reembolsar
|
||||||
|
|
||||||
|
1. Buscar la solicitud por:
|
||||||
|
- ID de solicitud
|
||||||
|
- Nombre del dueno
|
||||||
|
- Numero de operacion de Mercado Pago
|
||||||
|
2. Verificar que esta en estado "Pagado"
|
||||||
|
3. Verificar que el pago fue exitoso
|
||||||
|
|
||||||
|
## Pasos - Procesar reembolso
|
||||||
|
|
||||||
|
1. Abrir detalle de la solicitud
|
||||||
|
2. Click en "Procesar reembolso"
|
||||||
|
3. Ver informacion del pago:
|
||||||
|
- Monto pagado
|
||||||
|
- Fecha de pago
|
||||||
|
- Metodo de pago
|
||||||
|
- ID de operacion MP
|
||||||
|
4. Seleccionar tipo de reembolso:
|
||||||
|
- Total
|
||||||
|
- Parcial (ingresar monto)
|
||||||
|
5. Ingresar motivo del reembolso
|
||||||
|
6. Confirmar
|
||||||
|
7. Sistema procesa reembolso en Mercado Pago
|
||||||
|
8. Actualizar estado de la solicitud
|
||||||
|
|
||||||
|
## Pasos - Verificar reembolso
|
||||||
|
|
||||||
|
1. Ir a seccion "Pagos" o historial de la solicitud
|
||||||
|
2. Verificar que el reembolso fue procesado
|
||||||
|
3. Ver estado del reembolso en Mercado Pago
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Reembolso procesado en Mercado Pago
|
||||||
|
- Dueno recibe el dinero (segun tiempos de MP)
|
||||||
|
- Solicitud actualiza estado
|
||||||
|
- Registro en el historial
|
||||||
|
- Dueno notificado por email
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- Reembolso parcial no soportado por el metodo de pago
|
||||||
|
- Plazo de reembolso vencido (politica de MP)
|
||||||
|
- Dueno reclama que no recibio el dinero
|
||||||
|
- Reembolso duplicado por error
|
||||||
|
- Pago original en disputa
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Reembolso por mala atencion
|
||||||
|
- Reembolso parcial (solo algunos servicios)
|
||||||
|
- Visita incompleta (reembolso proporcional)
|
||||||
|
- Dueno quiere credito en vez de reembolso
|
||||||
|
- Reembolso fuera de plazo (solucion alternativa)
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Gestion de solicitudes
|
||||||
|
- Cancelacion de turno
|
||||||
|
- Atencion al cliente
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- API Mercado Pago: `POST /v1/payments/{id}/refunds`
|
||||||
|
- Tiempos de acreditacion:
|
||||||
|
- Tarjeta credito: 1-2 estados de cuenta
|
||||||
|
- Tarjeta debito: 5-10 dias habiles
|
||||||
|
- Dinero en cuenta MP: inmediato
|
||||||
|
- Plazo maximo para reembolso: 180 dias desde el pago
|
||||||
|
- Se debe guardar log de todos los reembolsos
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Reportes y Dashboard
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Administrador / Gerencia
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Backoffice `/admin/` -> Dashboard o Seccion "Reportes"
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Ver metricas del negocio, generar reportes y analizar datos
|
||||||
|
|
||||||
|
## Pasos - Ver dashboard
|
||||||
|
|
||||||
|
1. Acceder al backoffice
|
||||||
|
2. Ver dashboard principal con:
|
||||||
|
- Solicitudes del dia/semana/mes
|
||||||
|
- Ingresos del periodo
|
||||||
|
- Visitas completadas
|
||||||
|
- Conversion (solicitudes -> pagos)
|
||||||
|
- Vets activos
|
||||||
|
|
||||||
|
## Pasos - Generar reporte de solicitudes
|
||||||
|
|
||||||
|
1. Ir a "Reportes" -> "Solicitudes"
|
||||||
|
2. Seleccionar rango de fechas
|
||||||
|
3. Filtrar por:
|
||||||
|
- Estado
|
||||||
|
- Zona
|
||||||
|
- Veterinario
|
||||||
|
- Tipo de servicio
|
||||||
|
4. Click "Generar"
|
||||||
|
5. Ver tabla con resultados
|
||||||
|
6. Opcion de exportar (CSV/Excel)
|
||||||
|
|
||||||
|
## Pasos - Generar reporte de ingresos
|
||||||
|
|
||||||
|
1. Ir a "Reportes" -> "Ingresos"
|
||||||
|
2. Seleccionar periodo
|
||||||
|
3. Agrupar por:
|
||||||
|
- Dia/Semana/Mes
|
||||||
|
- Veterinario
|
||||||
|
- Servicio
|
||||||
|
- Zona
|
||||||
|
4. Ver grafico y tabla
|
||||||
|
5. Exportar si es necesario
|
||||||
|
|
||||||
|
## Pasos - Reporte de veterinarios
|
||||||
|
|
||||||
|
1. Ir a "Reportes" -> "Veterinarios"
|
||||||
|
2. Ver para cada vet:
|
||||||
|
- Visitas completadas
|
||||||
|
- Calificacion promedio
|
||||||
|
- Ingresos generados
|
||||||
|
- Tasa de cancelacion
|
||||||
|
3. Filtrar por periodo
|
||||||
|
4. Ordenar por metrica
|
||||||
|
|
||||||
|
## Pasos - Exportar datos
|
||||||
|
|
||||||
|
1. En cualquier reporte, click "Exportar"
|
||||||
|
2. Seleccionar formato (CSV, Excel)
|
||||||
|
3. Descargar archivo
|
||||||
|
4. Usar para analisis externo o contabilidad
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Vision clara del estado del negocio
|
||||||
|
- Datos para toma de decisiones
|
||||||
|
- Reportes exportables para contabilidad
|
||||||
|
- Identificacion de tendencias
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- Datos no actualizados en tiempo real
|
||||||
|
- Filtros que no devuelven lo esperado
|
||||||
|
- Discrepancia entre reportes y realidad
|
||||||
|
- Exportacion con formato incorrecto
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Reporte de prueba vs produccion
|
||||||
|
- Datos historicos antes de implementar filtro
|
||||||
|
- Comparativa año vs año
|
||||||
|
- Reporte personalizado para inversores
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Gestion de solicitudes (fuente de datos)
|
||||||
|
- Facturacion AFIP
|
||||||
|
- Google Sheets sync
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- API:
|
||||||
|
- `GET /mascotas/api/v1/stats-summary` (metricas principales)
|
||||||
|
- `GET /solicitudes/api/v1/service-requests/?date_from=X&date_to=Y` (datos crudos)
|
||||||
|
- Google Sheets: sincronizacion automatica via Celery
|
||||||
|
- Dashboard actualiza cada 5 minutos
|
||||||
|
- Reportes pesados: generar async y notificar cuando estan listos
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Registro de Usuario
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Dueno de mascota (nuevo)
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Pagina principal -> Boton "Registrarse" o `/register`
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Crear una cuenta nueva para poder reservar turnos y gestionar sus mascotas
|
||||||
|
|
||||||
|
## Pasos
|
||||||
|
|
||||||
|
1. Click en "Registrarse" en la pagina principal
|
||||||
|
2. Ingresar email
|
||||||
|
3. Ingresar contraseña
|
||||||
|
4. Confirmar contraseña
|
||||||
|
5. Aceptar terminos y condiciones
|
||||||
|
6. Click en "Crear cuenta"
|
||||||
|
7. Recibir email de verificacion
|
||||||
|
8. Click en el link de verificacion
|
||||||
|
9. Cuenta activada, redirige al dashboard
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Usuario creado en el sistema
|
||||||
|
- Email de bienvenida enviado
|
||||||
|
- Puede iniciar sesion inmediatamente
|
||||||
|
- Ve su dashboard vacio (sin mascotas, sin turnos)
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- El email ya esta registrado pero el usuario no lo recuerda
|
||||||
|
- El email de verificacion llega a spam
|
||||||
|
- La contraseña no cumple los requisitos minimos
|
||||||
|
- El usuario cierra el browser antes de verificar y no encuentra el email despues
|
||||||
|
- Usuarios intentan registrarse con email de otra persona
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Si el email existe como usuario invitado (del turnero), deberia linkear las cuentas
|
||||||
|
- Si el usuario ya tiene cuenta pero no verificada, reenviar email de verificacion
|
||||||
|
- Registro desde el flujo de turnero (ya ingreso datos, solo falta contraseña)
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Login (despues de registrarse)
|
||||||
|
- Turnero (puede disparar registro)
|
||||||
|
- Recuperar contraseña (si ya tenia cuenta y no recuerda)
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- API: `POST /common/users/create/`
|
||||||
|
- Validacion de contraseña: minimo 8 caracteres, 1 numero, 1 mayuscula
|
||||||
|
- Verificacion expira en 24 horas
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Reservar Turno (Turnero)
|
||||||
|
|
||||||
|
## Tipo de usuario
|
||||||
|
Dueno de mascota (invitado, registrado, o recurrente)
|
||||||
|
|
||||||
|
## Donde empieza
|
||||||
|
Pagina principal -> Boton "Agendar visita" o enlace directo `/turnero`
|
||||||
|
|
||||||
|
## Que quiere hacer el usuario
|
||||||
|
Reservar un turno veterinario a domicilio para su mascota
|
||||||
|
|
||||||
|
## Pasos
|
||||||
|
|
||||||
|
1. Click en "Agendar visita"
|
||||||
|
2. Ingresar direccion en el popup de cobertura
|
||||||
|
3. Sistema verifica si hay cobertura en esa zona
|
||||||
|
4. Si hay cobertura, click "Siguiente"
|
||||||
|
5. Completar datos de la mascota:
|
||||||
|
- Nombre
|
||||||
|
- Tipo (Perro/Gato)
|
||||||
|
- Raza (opcional)
|
||||||
|
- Edad aproximada
|
||||||
|
- Peso aproximado
|
||||||
|
- Si esta castrado/a
|
||||||
|
6. Seleccionar categoria de servicio (Vacunacion, Consulta, etc.)
|
||||||
|
7. Seleccionar servicios especificos
|
||||||
|
8. Ver que algunos servicios se agregan automaticamente (ej: consulta clinica)
|
||||||
|
9. Seleccionar fechas preferidas (hasta 3)
|
||||||
|
10. Seleccionar franja horaria preferida
|
||||||
|
11. Ingresar datos de contacto:
|
||||||
|
- Nombre completo
|
||||||
|
- Telefono
|
||||||
|
- Email
|
||||||
|
12. Revisar resumen
|
||||||
|
13. Enviar solicitud
|
||||||
|
|
||||||
|
## Que deberia pasar
|
||||||
|
|
||||||
|
- Solicitud creada con estado "Pendiente"
|
||||||
|
- Email de confirmacion al usuario
|
||||||
|
- Notificacion a veterinarios de la zona
|
||||||
|
- Aparece en "Mis turnos" del usuario
|
||||||
|
|
||||||
|
## Problemas comunes
|
||||||
|
|
||||||
|
- No hay cobertura en la zona del usuario
|
||||||
|
- El autocompletado de direcciones no encuentra la calle
|
||||||
|
- Usuario no puede quitar "Consulta clinica" cuando selecciona vacunacion
|
||||||
|
- Confunden "fecha preferida" con "fecha confirmada"
|
||||||
|
- No entienden por que no pueden elegir hora exacta
|
||||||
|
- El total no se muestra hasta el final y sorprende al usuario
|
||||||
|
|
||||||
|
## Casos especiales
|
||||||
|
|
||||||
|
- Usuario invitado: se crea como guest, puede registrarse despues
|
||||||
|
- Usuario logueado: datos pre-llenados, mascota puede ser existente
|
||||||
|
- Usuario recurrente (mismo email): sistema detecta y sugiere linkear
|
||||||
|
- Mascota ya castrada: no mostrar servicio de castracion
|
||||||
|
- Servicios combo: algunos incluyen otros automaticamente
|
||||||
|
- Zona sin cobertura: mostrar formulario para avisar cuando haya
|
||||||
|
|
||||||
|
## Flujos relacionados
|
||||||
|
|
||||||
|
- Ver mis turnos (despues de reservar)
|
||||||
|
- Pagar turno (cuando se coordina)
|
||||||
|
- Cancelar turno
|
||||||
|
- Agregar mascota (si ya tiene cuenta)
|
||||||
|
|
||||||
|
## Notas tecnicas
|
||||||
|
|
||||||
|
- APIs:
|
||||||
|
- `POST /mascotas/api/v1/coverage-check/` (verificar cobertura)
|
||||||
|
- `POST /mascotas/api/v1/pet-owners/` (crear dueno invitado)
|
||||||
|
- `POST /mascotas/api/v1/pets/` (crear mascota)
|
||||||
|
- `GET /productos/api/v1/services/` (obtener servicios)
|
||||||
|
- `POST /solicitudes/api/v1/service-requests/` (crear solicitud)
|
||||||
|
- Estados de solicitud: pending -> coordinated -> payed -> completed
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user