Skip to content

feat: add announcement banner - #3189

Open
dschmidt wants to merge 10 commits into
mainfrom
feat/announcement-banner
Open

feat: add announcement banner#3189
dschmidt wants to merge 10 commits into
mainfrom
feat/announcement-banner

Conversation

@dschmidt

@dschmidt dschmidt commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Runtime managed announcement banner shown above the top bar to all users, with a dialog showing Markdown detail text.

Model: { enabled, bannerText, infoText }, persisted in a NATS JetStream key-value store.

  • GET /announcement (authenticated, Announcement.ReadWrite permission): the full stored state, for the admin UI.
  • PUT /announcement: upsert; an empty banner text removes the record. There is no DELETE endpoint (an empty banner text is the implicit delete, and enabled hides/shows without deleting).
  • config.json: options.announcement.{bannerText, infoText} is injected only while enabled and a banner text are set, so prepared/disabled announcements stay private. The store is the single source of truth: config.json always reflects it, and a statically configured web.config.options.announcement is ignored (the field is yaml:"-", output only).

Public by design

The message is part of the unauthenticated config.json (loaded before login), so the banner also shows on the login and other pre-auth pages, e.g. to announce that a login provider is unavailable. Admins must not put sensitive information into it.

Web frontend: opencloud-eu/web#2967

@codacy-production

codacy-production Bot commented Jul 28, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 69 complexity · -30 duplication

Metric Results
Complexity 69
Duplication -30

View in Codacy

🟢 Coverage 7.72% diff coverage · -0.26% coverage variation

Metric Results
Coverage variation -0.26% coverage variation (-1.00%)
Diff coverage 7.72% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (d9e406c) 82331 19145 23.25%
Head commit (341ebdf) 83754 (+1423) 19256 (+111) 22.99% (-0.26%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#3189) 1438 111 7.72%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@dschmidt
dschmidt force-pushed the feat/announcement-banner branch 2 times, most recently from e339568 to 00ec8a2 Compare July 28, 2026 15:40
@dschmidt
dschmidt marked this pull request as ready for review July 28, 2026 15:43
@dschmidt
dschmidt force-pushed the feat/announcement-banner branch from 00ec8a2 to 501ab78 Compare July 28, 2026 16:04
@dschmidt
dschmidt requested a review from Copilot July 28, 2026 22:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a runtime-managed “announcement banner” feature to the web service, allowing admins to publish a message that is surfaced via the public config.json (so it can display pre-auth, including on the login page) and persisted in a configurable key-value store (defaulting to NATS).

Changes:

  • Introduces an announcement package with a persistent store abstraction and HTTP handlers guarded by a new Announcement.Write permission.
  • Injects the stored (enabled) announcement into the web config.json response at request time.
  • Adds configuration/defaults and proxy/settings wiring for the new store-backed settings and permission.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
services/web/pkg/service/v0/service.go Wires announcement routes and injects announcement into config.json payload.
services/web/pkg/service/v0/service_test.go Adds unit tests for the web service’s announcement selection helper.
services/web/pkg/service/v0/option.go Adds AnnouncementStore option plumbing into the web service constructor.
services/web/pkg/server/http/server.go Instantiates the persistent store and passes it into the web service.
services/web/pkg/config/options.go Extends web options with an announcement object and defines its schema.
services/web/pkg/config/defaults/defaultconfig.go Adds default store configuration for the web service.
services/web/pkg/config/config.go Adds Store configuration section to the web service config.
services/web/pkg/announcement/service.go Implements announcement management HTTP handlers with permission checks and body-size limit.
services/web/pkg/announcement/announcement.go Implements the store-backed persistence for a single announcement record.
services/web/pkg/announcement/announcement_test.go Adds Ginkgo/Gomega tests for store behavior and service authorization/body handling.
services/web/pkg/announcement/announcement_suite_test.go Adds Ginkgo test suite bootstrap for the announcement package.
services/settings/pkg/store/defaults/permissions.go Defines the new Announcement.Write permission.
services/settings/pkg/store/defaults/defaults.go Grants Announcement.Write to relevant default bundles/roles (incl. admin).
services/proxy/pkg/config/defaults/defaultconfig.go Routes /announcement to the web service via proxy defaults.
Comments suppressed due to low confidence (1)

services/web/pkg/service/v0/service.go:173

  • currentAnnouncement() currently returns nil for both “no stored announcement” and “stored but disabled”, which makes it impossible to distinguish between “fall back to static config” and “explicitly clear/disable any static announcement”. Returning the static config value as a fallback when the store is missing/empty/unavailable, and returning nil only when the stored announcement explicitly disables it, avoids that ambiguity.
func (p Web) currentAnnouncement() *config.Announcement {
	if p.announcementStore == nil {
		return nil
	}


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/web/pkg/service/v0/service.go
Comment thread services/web/pkg/service/v0/service.go Outdated
dschmidt added 4 commits July 29, 2026 08:18
Always reflect the stored announcement in config.json: expose it while live
and clear it otherwise. A disabled announcement now clears the config output
instead of falling back to a statically configured one, which removes the
precedence ambiguity raised in review.
Make the store authoritative only once it manages an announcement: an
enabled record is exposed, a disabled record clears any static config, and
an empty store falls back to a statically configured
web.config.options.announcement. This matches the review request and stops
a static announcement from being silently dropped.
The permission gates reading the full announcement state (including
disabled ones) as well as writing it, so name it ReadWrite to match the
other management permissions (Accounts.ReadWrite, Settings.ReadWrite, ...)
and use the READWRITE operation.
Distinguish the http.MaxBytesReader limit from a malformed body: an
oversized payload now returns 413 Request Entity Too Large instead of a
generic 400.
Comment thread services/web/pkg/announcement/service.go
Comment thread services/web/pkg/service/v0/service.go
Comment thread services/web/pkg/announcement/announcement.go Outdated
dschmidt added 2 commits July 29, 2026 10:08
Add a logger to the announcement service and log server-side errors before
returning a 5xx, tagged with the request id so they correlate with the
request log line from the logging middleware.
Replace the go-micro store with a NATS JetStream key-value bucket, created via
reva's cache.NewNatsKeyValue, so the web service no longer depends on go-micro
stores. The store methods now take a context, threaded through the config.json
render path and the management handlers. Tests use a mockery mock of
jetstream.KeyValue.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • services/web/mocks/key_value.go: Generated file
  • services/web/mocks/key_value_entry.go: Generated file
Comments suppressed due to low confidence (4)

services/web/pkg/service/v0/service.go:95

  • The /announcement routes are mounted unconditionally. If the announcement service/store is optional, this should be guarded (otherwise the handler will be nil / not initialized).
		r.Route("/announcement", func(r chi.Router) {
			r.Use(middleware.ExtractAccountUUID(
				account.Logger(options.Logger),
				account.JWTSecret(options.Config.TokenManager.JWTSecret),
			))

services/web/pkg/service/v0/service.go:80

  • announcement.NewService currently runs unconditionally and fails when AnnouncementStore isn't provided (options.AnnouncementStore is nil by default). If the runtime store is intended to be optional, service creation should be conditional so the web service can still start and serve static config.json without runtime announcement management.

This issue also appears on line 91 of the same file.

	announcementService, err := announcement.NewService(
		announcement.ServiceOptions{}.
			WithLogger(options.Logger).
			WithStore(options.AnnouncementStore).
			WithGatewaySelector(options.GatewaySelector),

services/web/pkg/announcement/service.go:20

  • PR description mentions an Announcement.Write permission for these endpoints, but the implementation checks for Announcement.ReadWrite (and settings defaults also register Announcement.ReadWrite). Please align the PR description/API docs with the implemented permission name (or rename the permission consistently across code + defaults if Announcement.Write is the intended contract).
// _permission is the settings permission required to read and manage the announcement.
const _permission = "Announcement.ReadWrite"

services/web/pkg/config/config.go:41

  • The new Store config fields use introductionVersion:"%%NEXT%%" placeholders. This placeholder isn’t used elsewhere in the repo and will likely leak into generated configuration docs. Please replace with the actual introduction version (or the project’s standard placeholder token, if any).
// Store configures the NATS JetStream key-value store used to keep runtime managed web settings,
// e.g. the announcement banner.
type Store struct {
	Nodes                []string `yaml:"nodes" env:"OC_PERSISTENT_STORE_NODES;WEB_STORE_NODES" desc:"A list of nodes to access the NATS JetStream store. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"`
	Database             string   `yaml:"database" env:"WEB_STORE_DATABASE" desc:"The bucket name the store should use." introductionVersion:"%%NEXT%%"`
	AuthUsername         string   `yaml:"username" env:"OC_PERSISTENT_STORE_AUTH_USERNAME;WEB_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store." introductionVersion:"%%NEXT%%"`
	AuthPassword         string   `yaml:"password" env:"OC_PERSISTENT_STORE_AUTH_PASSWORD;WEB_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store." introductionVersion:"%%NEXT%%"`
	EnableTLS            bool     `yaml:"enable_tls" env:"OC_PERSISTENT_STORE_ENABLE_TLS;WEB_STORE_ENABLE_TLS" desc:"Enable TLS for the connection to the store." introductionVersion:"%%NEXT%%"`
	TLSInsecure          bool     `yaml:"tls_insecure" env:"OC_INSECURE;OC_PERSISTENT_STORE_TLS_INSECURE;WEB_STORE_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"`
	TLSRootCACertificate string   `yaml:"tls_root_ca_certificate" env:"OC_PERSISTENT_STORE_TLS_ROOT_CA_CERTIFICATE;WEB_STORE_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided WEB_STORE_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"`

Comment thread services/web/pkg/server/http/server.go Outdated
dschmidt added 3 commits July 29, 2026 11:52
A statically configured announcement is not supported: it cannot be removed via
the runtime API and disappears from the running session once the admin settings
load (they read the store, not config.json). Make the store the single source
of truth, config.json always reflects it and a statically configured
options.announcement is ignored.
Replace reva's cache.NewNatsKeyValue (which retries with backoff and can hang
the web service startup) with a direct nats.Connect + jetstream setup like the
graph service, so an unreachable or misconfigured store surfaces as a clear
startup error instead of a hang.
The announcement is runtime-managed and injected into config.json; it is not a
static yaml config option. Exclude it from yaml (yaml:"-") so it is not
presented as a configurable knob, keeping the json tag only for the config.json
output.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated no new comments.

Files not reviewed (2)
  • services/web/mocks/key_value.go: Generated file
  • services/web/mocks/key_value_entry.go: Generated file
Comments suppressed due to low confidence (3)

services/web/pkg/service/v0/service.go:163

  • The PR description says a statically configured web.config.options.announcement is kept when the runtime store is empty/unavailable, but this code always overwrites options.announcement with the runtime value (and Announcement is yaml:"-", so static config is effectively unsupported). Please align the PR description/contract with the implemented runtime-only behavior.
	// the runtime store is the single source of truth for the announcement banner: expose it
	// when live, clear it otherwise. A statically configured value is not supported.
	webConfig.Options.Announcement = p.currentAnnouncement(ctx)

services/web/pkg/announcement/service.go:20

  • The PR description states the endpoint is protected by an Announcement.Write permission, but the implementation checks for Announcement.ReadWrite (and the settings defaults added the same). Please update the PR description (or rename the permission) so operators/admin UI know which permission to grant.
// _permission is the settings permission required to read and manage the announcement.
const _permission = "Announcement.ReadWrite"

services/web/pkg/server/http/server.go:95

  • The server establishes a long-lived NATS connection for the announcement store but never drains/closes it on shutdown. This can leave subscriptions/IO hanging and makes graceful stop/restart noisier in operations and tests. Register an AfterStop hook to drain the connection when the service stops.
	natsConn, err := nats.Connect(
		strings.Join(options.Config.Store.Nodes, ","),
		natspkg.Secure(options.Config.Store.EnableTLS, options.Config.Store.TLSInsecure, options.Config.Store.TLSRootCACertificate),
		nats.UserInfo(options.Config.Store.AuthUsername, options.Config.Store.AuthPassword),
	)
	if err != nil {
		return http.Service{}, fmt.Errorf("could not connect to nats for the announcement store: %w", err)
	}

@aduffeck aduffeck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 😍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants