Make main the lean product a site receives
main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
This commit is contained in:
@@ -10,6 +10,18 @@ class ConfigError(Exception):
|
||||
"""Raised when required configuration is missing or unsafe."""
|
||||
|
||||
|
||||
# Connection pool per worker process. Env-driven so a site can size it against
|
||||
# its own MySQL max_connections without editing code; see the arithmetic in
|
||||
# docker-compose.yml next to the worker count.
|
||||
#
|
||||
# Deliberately NOT named SQLALCHEMY_POOL_SIZE and friends: those were
|
||||
# Flask-SQLAlchemy 2.x config keys, removed in 3.x, and reusing the names would
|
||||
# suggest the extension still reads them. It reads SQLALCHEMY_ENGINE_OPTIONS.
|
||||
DB_POOL_SIZE = int(os.environ.get('DB_POOL_SIZE', 10))
|
||||
DB_MAX_OVERFLOW = int(os.environ.get('DB_MAX_OVERFLOW', 10))
|
||||
DB_POOL_TIMEOUT = int(os.environ.get('DB_POOL_TIMEOUT', 5))
|
||||
|
||||
|
||||
def _required_env(varname):
|
||||
"""Read an env var; raise ConfigError if missing or empty."""
|
||||
value = os.environ.get(varname)
|
||||
@@ -32,9 +44,32 @@ class Config:
|
||||
'mysql+pymysql://root:password@localhost:3306/shopdb_flask',
|
||||
)
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# Connection pool, sized on purpose rather than left to SQLAlchemy.
|
||||
#
|
||||
# The defaults (pool_size 5, max_overflow 10, pool_timeout 30) were never
|
||||
# chosen: they simply applied, and nothing in the repo related them to the
|
||||
# gunicorn worker count or to the MySQL max_connections ceiling. The
|
||||
# arithmetic that ties the three together is written next to the worker
|
||||
# count in docker-compose.yml.
|
||||
#
|
||||
# Under gevent a pooled connection is held for the duration of whatever the
|
||||
# greenlet is doing, so a request that sits inside a slow external HTTP call
|
||||
# WITH THE SESSION OPEN pins a connection for that whole call. Hundreds of
|
||||
# greenlets can enter such a path at once and drain the pool, at which point
|
||||
# every other request blocks on checkout. Hence the fail-fast timeout: 5
|
||||
# seconds, not 30. A pool that is empty for five seconds is not going to be
|
||||
# fine in thirty, and a fast 500 is diagnosable where a 30-second hang looks
|
||||
# like a dead database.
|
||||
#
|
||||
# See PLUGIN-DEVELOPMENT / the plugin docs: a plugin must not hold a DB
|
||||
# session across external I/O.
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_pre_ping': True,
|
||||
'pool_recycle': 300,
|
||||
'pool_size': DB_POOL_SIZE,
|
||||
'max_overflow': DB_MAX_OVERFLOW,
|
||||
'pool_timeout': DB_POOL_TIMEOUT,
|
||||
# Pin the client connection to utf8mb4 so reads/writes match the
|
||||
# utf8mb4 schema regardless of the server default charset. TestingConfig
|
||||
# (SQLite) replaces this whole dict, so the charset arg never reaches it.
|
||||
@@ -130,15 +165,6 @@ class Config:
|
||||
EMPLOYEE_DB_PASSWORD = os.environ.get('EMPLOYEE_DB_PASSWORD', '')
|
||||
EMPLOYEE_DB_NAME = os.environ.get('EMPLOYEE_DB_NAME', 'wjf_employees')
|
||||
|
||||
# Read-write CMMC USB check-in/out database (separate from the app DB).
|
||||
# Credentials come from the environment; never hardcode them in source.
|
||||
# No safe default for the password: unset means empty, the connection
|
||||
# fails loud rather than silently trying a guessed credential.
|
||||
CMMC_USB_DB_HOST = os.environ.get('CMMC_USB_DB_HOST', 'localhost')
|
||||
CMMC_USB_DB_USER = os.environ.get('CMMC_USB_DB_USER', '')
|
||||
CMMC_USB_DB_PASSWORD = os.environ.get('CMMC_USB_DB_PASSWORD', '')
|
||||
CMMC_USB_DB_NAME = os.environ.get('CMMC_USB_DB_NAME', 'cmmc_usb')
|
||||
|
||||
CACHE_TYPE = 'SimpleCache'
|
||||
CACHE_DEFAULT_TIMEOUT = 600
|
||||
|
||||
@@ -150,6 +176,22 @@ class Config:
|
||||
AUTH_RATELIMIT_WINDOW_SECONDS = int(
|
||||
os.environ.get('AUTH_RATELIMIT_WINDOW_SECONDS', 300))
|
||||
|
||||
# Swagger UI "try it" page at /api/docs/try, beside the read-only Redoc
|
||||
# page at /api/docs. One spec, two renderers.
|
||||
#
|
||||
# Off unless a site turns it on, and that is the point rather than caution:
|
||||
# Swagger UI's Execute button fires a REAL request at THIS server from the
|
||||
# operator's browser, carrying whatever session that browser already holds.
|
||||
# On a production box that is a one-click way to POST against live fleet
|
||||
# data from a page anyone can open. Redoc, which cannot send anything, stays
|
||||
# the default and is always on.
|
||||
#
|
||||
# When off the routes are never bound at all, so /api/docs/try is Flask's
|
||||
# own 404 and the Redoc page does not mention it. See
|
||||
# shopdb/core/api/docs.py.
|
||||
API_DOCS_TRYIT_ENABLED = os.environ.get(
|
||||
'API_DOCS_TRYIT_ENABLED', 'false').lower() == 'true'
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
MAX_PAGE_SIZE = 100
|
||||
|
||||
@@ -163,11 +205,14 @@ class Config:
|
||||
# three different routes, which makes it a property of the app rather than
|
||||
# of any handler.
|
||||
#
|
||||
# 128 MB clears the largest thing anyone legitimately sends - the agent
|
||||
# binary upload caps itself at 64 MB - with room for the multipart envelope.
|
||||
# 256 MB clears the largest thing anyone legitimately sends - the agent
|
||||
# binary upload caps itself at 128 MB - with room for the multipart
|
||||
# envelope. This must stay STRICTLY ABOVE the agent cap: it bounds the
|
||||
# whole request (file + boundaries + form fields), so setting the two
|
||||
# equal would reject a file sitting exactly at the agent limit.
|
||||
# Over this, Werkzeug answers 413 without reading the body.
|
||||
MAX_CONTENT_LENGTH = int(
|
||||
os.environ.get('MAX_CONTENT_LENGTH', 128 * 1024 * 1024))
|
||||
os.environ.get('MAX_CONTENT_LENGTH', 256 * 1024 * 1024))
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
@@ -176,6 +221,12 @@ class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
SQLALCHEMY_ECHO = True
|
||||
|
||||
# On in development: firing the request is the whole reason a developer
|
||||
# opens the API docs. Still read from the environment, so a shared dev
|
||||
# instance can turn it back off without editing this file.
|
||||
API_DOCS_TRYIT_ENABLED = os.environ.get(
|
||||
'API_DOCS_TRYIT_ENABLED', 'true').lower() == 'true'
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||
'DATABASE_URL',
|
||||
'mysql+pymysql://root:changeme@127.0.0.1:3306/shopdb_flask',
|
||||
@@ -189,6 +240,23 @@ class TestingConfig(Config):
|
||||
# Off by default so login-heavy fixtures do not trip the limiter; tests
|
||||
# that exercise rate limiting flip it on via app.config override.
|
||||
AUTH_RATELIMIT_ENABLED = False
|
||||
# Same reason: the enrollment suites post to /enroll dozens of times, and
|
||||
# the limiter counts in a process-wide cache that outlives a test, so with
|
||||
# it on by default a budget spent in one module surfaces as an unrelated
|
||||
# failure in another - and which modules share a worker depends on the
|
||||
# xdist distribution, so the failure moves around. The tests that are about
|
||||
# the limiter turn it on themselves.
|
||||
ENDPOINTS_ENROLL_RATELIMIT_ENABLED = False
|
||||
# Off, and stated here rather than inherited, because a test suite that
|
||||
# silently ran with it on would be testing a configuration no site ships.
|
||||
# The session-wide app in tests/conftest.py is what
|
||||
# tests/test_core/test_public_read_surface.py sweeps for ungated GET
|
||||
# routes, and the try-it page plus its two vendored assets are
|
||||
# deliberately unauthenticated. Keeping the default here means that sweep
|
||||
# keeps describing what production actually exposes; the tests that ARE
|
||||
# about the page build their own app with the flag on
|
||||
# (tests/test_core/test_api_docs_tryit.py).
|
||||
API_DOCS_TRYIT_ENABLED = False
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'connect_args': {'check_same_thread': False},
|
||||
|
||||
Reference in New Issue
Block a user