# Package settings

An installable app declares its configuration as one typed Go struct. The project
passes the values in Go; a deployment may override tagged fields with environment
variables under the app's own prefix, `CICADEL_<APP>_<NAME>`. The application
resolves and validates the settings before `Start`, `manage.go check` reports an
invalid key without printing its value, and secret fields are never displayed.
There are no global settings: the app reads its resolved struct directly.

## Declare settings

Tag each field a deployment may change with `setting:"NAME"`. Add `,secret` for
credentials. Leave handles and callbacks untagged; they are set only in Go.
Supported types are `string`, `bool`, integers, unsigned integers, floats,
`time.Duration` and `[]string` (comma separated).

<!-- example: installable/apps/newsletter/app.go -->
```go
package newsletter

import (
    "context"
    "errors"
    "net/http"
    "strings"
    "time"

    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/response"
    "github.com/fauzaanu/cicadel/settings"
    "github.com/fauzaanu/cicadel/urls"
)

// Options is configured by the project. A deployment may override tagged
// fields with CICADEL_NEWSLETTER_FROM, CICADEL_NEWSLETTER_BATCH,
// CICADEL_NEWSLETTER_INTERVAL and CICADEL_NEWSLETTER_API_KEY.
type Options struct {
    From     string        `setting:"FROM"`
    Batch    int           `setting:"BATCH"`
    Interval time.Duration `setting:"INTERVAL"`
    APIKey   string        `setting:"API_KEY,secret"`
}

// Validate runs after environment overrides. Name the field, never the value.
func (o Options) Validate() error {
    if !strings.Contains(o.From, "@") {
        return errors.New("From must be an email address")
    }
    if o.Batch < 1 || o.Batch > 1000 {
        return errors.New("Batch must be 1–1000")
    }
    return nil
}

func New(options Options) cicadel.App {
    if options.Batch == 0 {
        options.Batch = 100
    }
    config := settings.NewPackage(options)
    return cicadel.App{
        Name:     "newsletter",
        Settings: config,
        Start: func(context.Context) (cicadel.Cleanup, error) {
            _ = config.Value().Interval // resolved and valid here
            return nil, nil
        },
        Routes: []urls.Route{
            urls.Path("GET", "/newsletter/", "newsletter:index",
                func(w http.ResponseWriter, r *http.Request) {
                    if err := response.JSON(w, http.StatusOK, map[string]int{"batch": config.Value().Batch}); err != nil {
                        response.Error(w, http.StatusInternalServerError)
                    }
                }),
        },
    }
}
```

`settings.NewPackage` records the project's values without reading the
environment. Construct a fresh one inside the app constructor, as above, so each
application gets its own. If `Options` has a `Validate() error` method, it runs
after overrides.

## Configure them in the project

Install the app in `project/apps.go` with explicit Go values:

```go
newsletter.New(newsletter.Options{From: "news@example.com", Interval: time.Hour})
```

`cicadel.New` resolves every app's settings from the environment that
`settings.Load` read (`Settings.Env`), in dependency order. Override one per
deployment:

```sh
CICADEL_NEWSLETTER_BATCH=500 go run manage.go runserver
```

Settings from `settings.Defaults()` or `apptest.Settings()` read no
environment, so tests see only the Go values. Inject overrides in a test with
`settings.LoadWith(apptest.Settings(), lookup)`.

## Inspect and check

`go run manage.go apps` lists each key, its current value and where it came from.
Secret values print as `[redacted]`:

```text
newsletter (local)
  CICADEL_NEWSLETTER_FROM="news@example.com" (default)
  CICADEL_NEWSLETTER_BATCH="500" (environment)
  CICADEL_NEWSLETTER_INTERVAL="1h0m0s" (default)
  CICADEL_NEWSLETTER_API_KEY="[redacted]" (default)
```

An invalid value does not stop construction. `go run manage.go check` (and
`check --json`) reports it as `<app>:settings:invalid`, naming the key but not
the value, and `Start` refuses to run:

```text
error newsletter:settings:invalid: CICADEL_NEWSLETTER_BATCH must be an integer in range
```

Validation errors read `app newsletter settings: Batch must be 1–1000`.
`Application.PackageSettings` returns the same redacted description for tools.

## Rules

- Keys are always `CICADEL_<APP>_<NAME>`, with `NAME` in upper snake case.
  `cicadel.New` rejects a key that collides with a project setting
  (`settings.CoreKeys`) or with another app's key, a duplicate name and an
  unsupported field type.
- A system check named `settings` is reserved on apps that declare settings.
- Settings resolve once. Sharing one `settings.Package` between two apps is an
  error.
- A package that needs its values while it is being constructed resolves them
  itself with `Package.Resolve(app, s.Env())` and still sets `App.Settings` to the
  same value, so `apps` and `check` see it. `settings.LoadPackage` resolves
  without keeping the result.

## Accounts settings

The standard accounts app uses this contract. Generated projects set
`AccountOptions` in `project/accounts.go`, and a deployment may override
`CICADEL_ACCOUNTS_SESSION_LIFETIME`, `CICADEL_ACCOUNTS_RECOVERY_LIFETIME`,
`CICADEL_ACCOUNTS_IDLE_TIMEOUT` and `CICADEL_ACCOUNTS_BCRYPT_COST` (10–16). With
an invalid value the project builds without account services, so `check` reports
the key and `runserver` refuses to start.
