{"id":44889706,"url":"https://github.com/tyemirov/loopaware","last_synced_at":"2026-06-17T05:00:47.683Z","repository":{"id":337327845,"uuid":"1048650954","full_name":"tyemirov/loopaware","owner":"tyemirov","description":"Feedback Service to gather users'  feedback","archived":false,"fork":false,"pushed_at":"2026-06-09T23:59:37.000Z","size":18905,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-10T01:18:47.800Z","etag":null,"topics":["feedback","feedback-form","website"],"latest_commit_sha":null,"homepage":"https://loopaware.mprlab.com","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tyemirov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-09-01T19:48:48.000Z","updated_at":"2026-06-09T23:59:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"7c9b4847-a9c0-4805-92cb-ad9570ebd8ea","html_url":"https://github.com/tyemirov/loopaware","commit_stats":null,"previous_names":["tyemirov/loopaware"],"tags_count":39,"template":false,"template_full_name":null,"purl":"pkg:github/tyemirov/loopaware","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tyemirov%2Floopaware","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tyemirov%2Floopaware/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tyemirov%2Floopaware/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tyemirov%2Floopaware/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tyemirov","download_url":"https://codeload.github.com/tyemirov/loopaware/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tyemirov%2Floopaware/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34434496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["feedback","feedback-form","website"],"created_at":"2026-02-17T18:38:38.782Z","updated_at":"2026-06-17T05:00:47.674Z","avatar_url":"https://github.com/tyemirov.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LoopAware\n\n[![CI](https://github.com/tyemirov/loopaware/actions/workflows/ci.yml/badge.svg)](https://github.com/tyemirov/loopaware/actions/workflows/ci.yml)\n[![License: Source Available](https://img.shields.io/badge/License-Source%20Available-blue)](./LICENSE)\n[![Go 1.26](https://img.shields.io/badge/Go-1.26-00ADD8?logo=go)](https://go.dev)\n[![Latest Release](https://img.shields.io/github/v/release/tyemirov/loopaware)](https://github.com/tyemirov/loopaware/releases)\n\n**Privacy-first feedback widget, traffic analytics, and developer monitoring.** Drop a single script tag on your site to collect customer feedback, capture email subscribers, track visits, and report browser errors -- all backed by a role-aware dashboard and a self-hosted SQLite backend.\n\n- **Free** for personal and non-revenue projects\n- **Commercial license** required for revenue-generating use\n- See [LICENSE](./LICENSE) for details\n\n\u003c!-- TODO: Add screenshot of dashboard here --\u003e\n\u003c!-- ![LoopAware Dashboard](docs/screenshot-dashboard.png) --\u003e\n\n## Quick Start\n\n```bash\n# Clone and start the development stack\ngit clone https://github.com/tyemirov/loopaware.git\ncd loopaware\n./scripts/up.sh\n\n# Open the dashboard\nopen http://localhost:8080/login\n```\n\nEmbed the feedback widget on any page:\n\n```html\n\u003cscript src=\"https://loopaware.mprlab.com/widget.js?site_id=YOUR_SITE_ID\" defer\u003e\u003c/script\u003e\n```\n\n## Highlights\n\n- Shared `mpr-ui` sign-in with TAuth-issued sessions and TAuth verifier-backed API protection\n- Role-aware dashboard (`/app`) with admin, creator/owner, and per-site team-member scopes\n- YAML configuration for privileged accounts (`configs/config.loopaware.yml`)\n- REST API to create, update, and inspect sites, feedback, subscribers, and traffic\n- Background favicon refresh scheduler with live dashboard notifications\n- Embeddable JavaScript widget with strict origin validation\n- Email subscription capture via an embeddable subscribe form\n- Privacy-safe traffic pixel with per-site visit and visitor counts\n- Daily, weekly, or monthly traffic report emails delivered through Pinguin to a manager, the whole site team, or selected members\n- First-class LA Sentry developer error monitoring with protected server-to-server ingest and origin-bound browser capture\n- SQLite-first storage with pluggable drivers\n- Public privacy policy and compliance endpoints for visibility\n- Table-driven tests and fast in-memory SQLite fixtures\n\n## Configuration\n\n### 1. Admin roster (`configs/config.loopaware.yml`)\n\nEdit the tracked YAML file at `configs/config.loopaware.yml` with the email addresses that should receive administrator privileges (the file is optional if you prefer environment-only configuration):\n\n```yaml\nadmins:\n  - temirov@gmail.com\n```\n\nLoopAware loads the file specified by `--config` (default `configs/config.loopaware.yml`) before starting the HTTP server.\nSet the `ADMINS` environment variable with a comma-separated list (for example `ADMINS=alice@example.com,bob@example.com`) to override the YAML roster without editing the file. When neither source is present the server starts without administrators and records a warning in the logs.\n\n### 2. Environment variables\n\nBackend (`cmd/server`):\n\n| Variable               | Required | Description                                                 |\n|------------------------|----------|-------------------------------------------------------------|\n| `SESSION_SECRET`       | ✅        | 32+ byte secret for subscription confirmation tokens        |\n| `TAUTH_BASE_URL`       | ✅        | Base URL for the TAuth API                                  |\n| `TAUTH_TENANT_ID`      | ✅        | Tenant identifier configured in TAuth                       |\n| `TAUTH_JWT_SIGNING_KEY`| ✅        | JWT signing key used to validate `app_session`              |\n| `TAUTH_SESSION_COOKIE_NAME` | ⚙️   | Session cookie name set by TAuth (defaults to `app_session`) |\n| `PINGUIN_ADDR`         | ✅        | Pinguin gRPC address                                        |\n| `PINGUIN_AUTH_TOKEN`¹  | ✅        | Bearer token passed to the Pinguin gRPC service             |\n| `PINGUIN_TENANT_ID`    | ✅        | Tenant identifier used when calling the Pinguin gRPC API     |\n| `TRAFFIC_REPORT_EMAILS_ENABLED` | ⚙️ | Enables scheduled/test traffic report emails (default `true`) |\n| `ADMINS`               | ⚙️       | Comma-separated admin emails; overrides the YAML roster     |\n| `PUBLIC_BASE_URL`      | ⚙️       | Frontend origin used for CORS and subscription links        |\n| `APP_ADDR`             | ⚙️       | Listen address (default `:8080`)                            |\n| `DB_DRIVER`            | ⚙️       | Storage driver (`sqlite`, etc.)                             |\n| `DB_DSN`               | ⚙️       | Driver-specific DSN                                         |\n\nSecrets must come from the environment; only non-sensitive settings belong in `configs/config.loopaware.yml`.\n\nWhen running via Docker Compose, copy the tracked env templates under `configs/` and edit the local `.env.*` files:\n\n```bash\ncp configs/.env.loopaware.example configs/.env.loopaware\ncp configs/.env.tauth.example configs/.env.tauth\ncp configs/.env.pinguin.example configs/.env.pinguin\n$EDITOR configs/.env.loopaware configs/.env.tauth configs/.env.pinguin\n```\n\n¹Pinguin and LoopAware must share the **exact same** bearer secret. Provide identical values for `GRPC_AUTH_TOKEN` and `PINGUIN_AUTH_TOKEN`, for example:\n\n```dotenv\nGRPC_AUTH_TOKEN=loopaware-local-secret\nPINGUIN_AUTH_TOKEN=loopaware-local-secret\n```\n\nLoopAware falls back to `GRPC_AUTH_TOKEN` when `PINGUIN_AUTH_TOKEN` is empty, so exporting the shared value once at runtime also works.\n\n### 3. Flags\n\nAll configuration options are also exposed as Cobra flags:\n\n```\nloopaware --config=configs/config.loopaware.yml \\\n  --app-addr=:8080 \\\n  --db-driver=sqlite \\\n  --db-dsn=\"file:loopaware.sqlite?_foreign_keys=on\" \\\n  --session-secret=$SESSION_SECRET \\\n  --tauth-base-url=$TAUTH_BASE_URL \\\n  --tauth-tenant-id=$TAUTH_TENANT_ID \\\n  --tauth-signing-key=$TAUTH_JWT_SIGNING_KEY \\\n  --tauth-session-cookie-name=$TAUTH_SESSION_COOKIE_NAME \\\n  --traffic-report-emails=true \\\n  --public-base-url=https://feedback.example.com\n```\n\nFlags are optional when the equivalent environment variables are set.\n\n## Running locally\n\nFor Docker-based local development, use the helper script:\n\n```bash\n./scripts/up.sh\n```\n\nStop the local stack with:\n\n```bash\n./scripts/down.sh\n```\n\n`scripts/up.sh` is the canonical startup path for Dockerized LoopAware. With no argument it opens an interactive selector.\nYou can also call it explicitly as `./scripts/up.sh local` or `./scripts/up.sh computercat`.\nThe local compose stack now includes a gHTTP proxy that serves `web/` at `http://localhost:8080` and forwards `/api`,\n`/auth`, and `/public` to the backend services. That proxy is also responsible for the browser-facing\nsecurity headers on the static HTML and proxied API responses in the local stack.\n\nIf you want to run only the API process without Docker, use:\n\n```bash\nSESSION_SECRET=$(openssl rand -hex 32) \\\nTAUTH_BASE_URL=http://localhost:8081 \\\nTAUTH_TENANT_ID=loopaware \\\nTAUTH_JWT_SIGNING_KEY=replace-with-tauth-jwt-signing-key \\\nTAUTH_SESSION_COOKIE_NAME=loopaware_development_session \\\nPUBLIC_BASE_URL=http://localhost:8080 \\\ngo run ./cmd/server --config=configs/config.loopaware.yml\n```\n\nWhen serving the static frontend directly from `web/`, no preparation step is required. Keep the tracked runtime\nconfig in `web/config.yml` and serve `web/` from the frontend origin or reverse proxy that will answer `/config.yml`,\n`/api`, and `/auth`.\n\nThen open `/app` on that frontend origin to trigger the shared sign-in flow.\nEnsure the TAuth service is running at `TAUTH_BASE_URL` with a tenant that matches `TAUTH_TENANT_ID`.\nAdministrators listed in `configs/config.loopaware.yml` can manage every site; other users see sites they own, sites\nthey originally created with their authenticated account, or sites where an admin added their email as a team member.\n\nThe static frontend pins `mpr-ui` through CDN URLs and lets `mpr-ui` own browser authentication scaffolding. Do not copy\nthird-party browser bundles into `web/`; non-CDN frontend dependencies are forbidden by architecture.\n\n## Authentication flow\n\n1. Users visit `/login` (automatic redirect from protected routes).\n2. `mpr-ui` drives the browser sign-in lifecycle against the configured TAuth tenant.\n3. TAuth issues and refreshes the session cookie configured by `TAUTH_SESSION_COOKIE_NAME` (defaults to `app_session`).\n4. `api.AuthManager` validates the session with TAuth's verifier, injects user details into the request context, and enforces admin,\n   owner, or team-member site access.\n5. The dashboard and JSON APIs consume the authenticated context.\n\n## Static frontend\n\nLoopAware’s frontend lives in `web/` and is hosted separately (CDN or reverse proxy). It includes:\n\n- `/login` — landing page with shared `mpr-ui`/TAuth sign-in.\n- `/resources` — crawlable public resource index with focused product and use-case pages.\n- `/privacy` — static privacy policy linked from the landing and dashboard footers.\n- `/app` — dashboard shell (data loaded via `/api/*`).\n- `/subscriptions/confirm` and `/subscriptions/unsubscribe` — email link pages.\n- `/widget.js`, `/subscribe.js`, `/pixel.js`, `/la-sentry.js` — embeddable JavaScript assets.\n- `/sentry/errors` — protected server-to-server developer error ingest.\n- `/sentry/browser-errors` — origin-bound browser developer error ingest.\n\nThe repository does not vendor third-party browser dependencies into `web/`. External JavaScript and CSS, including UI\nlibraries, must be referenced through pinned CDN URLs. Any browser dependency that is not delivered by CDN is\nforbidden. `web/` is reserved for LoopAware-authored assets only, so deployments, cache behavior, and browser tests\nexercise the same delivery path used in production.\n\nSet `PUBLIC_BASE_URL` to the frontend origin so the API emits correct links and CORS allows browser access. Use\nabsolute `data-api-origin` attributes (or `api_origin` query params) on embed scripts when the API runs on a different\norigin. The dashboard and login pages call `/api` and `/auth` relative to the frontend origin, so split-origin\ndeployments should use a reverse proxy or update the static HTML in `web/` to point at those services.\nThe tracked runtime host mapping lives in `web/config.yml`, which `web/runtime-env.js` fetches directly at runtime.\nCanonical SEO metadata, Open Graph URLs, `robots.txt`, and `sitemap.xml` are fixed to the single public site\n`https://loopaware.mprlab.com` and are not environment-specific.\nEach environment may also define `services.siteWidgetSiteId` there to bootstrap the first-party feedback widget on\n`/login` and `/app` without hard-coding a site UUID into the static HTML.\n\n## REST API\n\nAll authenticated endpoints live under `/api` and require the TAuth session cookie configured by `TAUTH_SESSION_COOKIE_NAME`. Public collection endpoints for\nfeedback, subscriptions, and visits do not require a session but still enforce per-site origin rules. JSON responses\ninclude Unix timestamps in seconds.\n\n| Method  | Path                                  | Role        | Description                                                                                             |\n|---------|---------------------------------------|-------------|---------------------------------------------------------------------------------------------------------|\n| `GET`   | `/api/me`                             | any         | Current account metadata (email, name, `role`, `avatar.url`)                                            |\n| `GET`   | `/api/sites`                          | any         | Sites visible to the caller; each row includes `access_role` (`admin` or `team_member`)                  |\n| `POST`  | `/api/sites`                          | any         | Create a site (requires `name`, `allowed_origin`, `owner_email`)                                        |\n| `PATCH` | `/api/sites/:id`                      | owner/admin | Update name/origin; admins may reassign ownership                                                       |\n| `DELETE`| `/api/sites/:id`                      | owner/admin | Delete a site                                                                                            |\n| `GET`   | `/api/sites/:id/team`                 | owner/admin | List per-site team member email assignments                                                             |\n| `POST`  | `/api/sites/:id/team`                 | owner/admin | Add a per-site team member by email                                                                     |\n| `DELETE`| `/api/sites/:id/team/:member_id`      | owner/admin | Remove a per-site team member assignment                                                                |\n| `GET`   | `/api/sites/:id/mobile-apps`          | owner/admin/team member | List native mobile apps registered for feedback submissions                                    |\n| `POST`  | `/api/sites/:id/mobile-apps`          | owner/admin | Register a native mobile app for mobile feedback submissions                                            |\n| `GET`   | `/api/sites/:id/messages`             | owner/admin/team member | List feedback messages (newest first)                                                         |\n| `GET`   | `/api/sites/:id/subscribers`          | owner/admin/team member | List subscribers for a site                                                                   |\n| `GET`   | `/api/sites/:id/subscribers/export`   | owner/admin/team member | Download subscribers as CSV                                                                   |\n| `PATCH` | `/api/sites/:id/subscribers/:subscriber_id` | owner/admin | Update a subscriber’s status (confirm or unsubscribe)                                             |\n| `DELETE`| `/api/sites/:id/subscribers/:subscriber_id` | owner/admin | Delete a subscriber                                                                                |\n| `GET`   | `/api/sites/:id/visits/stats`         | owner/admin/team member | Aggregate visit and unique visitor counts plus recent visits and top pages (optional `interval=all\\|1day\\|30days`) |\n| `GET`   | `/api/sites/:id/visits/export`        | owner/admin/team member | Download traffic visits as CSV (optional `interval=all\\|1day\\|30days`)                      |\n| `GET`   | `/api/sites/:id/sentry/issues`        | owner/admin/team member | List grouped developer error issues for a site                                                |\n| `GET`   | `/api/sites/:id/sentry/issues/:issue_id` | owner/admin/team member | Inspect latest and recent LA Sentry error occurrences                                      |\n| `PATCH` | `/api/sites/:id/sentry/issues/:issue_id` | owner/admin | Update issue status (`unresolved`, `resolved`, or `ignored`)                                         |\n| `POST`  | `/api/sites/:id/sentry/token`         | owner/admin | Rotate and reveal a per-site LA Sentry ingest token                                                     |\n| `GET`   | `/api/sites/:id/visits/trend`         | owner/admin/team member | Daily visit trend (default 7 days, optional `days` query param up to 30, or `interval=all\\|1day\\|30days`) |\n| `GET`   | `/api/sites/:id/visits/attribution`   | owner/admin/team member | Source/medium/campaign attribution breakdown (optional `limit` query param up to 50; optional `interval=all\\|1day\\|30days`) |\n| `GET`   | `/api/sites/:id/visits/engagement`    | owner/admin/team member | Visitor engagement metrics (default 30 days, optional `days` query param up to 90, or `interval=all\\|1day\\|30days`) |\n| `GET`   | `/api/sites/:id/visits/devices`       | owner/admin/team member | Device, screen resolution, and viewport breakdowns (optional `limit` query param up to 50; optional `interval=all\\|1day\\|30days`) |\n| `GET`   | `/api/sites/:id/visits/locations`     | owner/admin/team member | Inferred visitor locations from edge geo, timezone, locale, network, or unknown signals with confidence metadata (optional `limit` query param up to 50; optional `interval=all\\|1day\\|30days`) |\n| `GET`   | `/api/sites/:id/traffic-report-schedule` | owner/admin | Read the selected-site traffic report schedule, including `recipient_mode` (`manager`, `team`, or `selected`) and selected team member emails |\n| `PUT`   | `/api/sites/:id/traffic-report-schedule` | owner/admin | Save the selected-site traffic report schedule; `recipient_mode: \"selected\"` accepts only current per-site team member emails in `recipient_emails` |\n| `POST`  | `/api/sites/:id/traffic-report-schedule/test` | owner/admin | Send the selected-site traffic report immediately to the schedule's resolved recipients |\n| `GET`   | `/api/sites/favicons/events`          | any         | Server-sent events stream announcing refreshed site favicons                                            |\n| `GET`   | `/api/sites/feedback/events`          | any         | Server-sent events stream announcing new feedback                                                      |\n| `POST`  | `/public/feedback`                       | public      | Submit feedback (requires `site_id`, valid `contact` as email or phone, and at least one of `message` or `sentiment`) |\n| `POST`  | `/public/mobile-feedback`                | public      | Submit feedback from a registered mobile app with screen/app context                                   |\n| `POST`  | `/public/subscriptions`                  | public      | Submit an email subscription (JSON body with `site_id`, `email`, optional `name` and `source_url`)      |\n| `POST`  | `/public/subscriptions/confirm`          | public      | Confirm a subscription for a given `site_id` and email                                                  |\n| `POST`  | `/public/subscriptions/unsubscribe`      | public      | Unsubscribe an email address for a given `site_id`                                                      |\n| `GET`   | `/public/visits`                         | public      | Record a page visit for a site (returns a 1×1 GIF for use as a tracking pixel)                          |\n| `POST`  | `/sentry/errors`                         | ingest token | Submit developer error events with `Authorization: Bearer \u003ctoken\u003e` or `X-LoopAware-Sentry-Token`       |\n| `POST`  | `/sentry/browser-errors`                 | site origin | Submit browser JavaScript error events from configured site origins                                     |\n\nSubscriptions use confirmation and unsubscribe links sent via email: the static frontend pages at\n`/subscriptions/confirm?token=...` and `/subscriptions/unsubscribe?token=...` call the API without requiring browser\norigin headers.\n\nLA Sentry ingest accepts JSON with `site_id`, `event_id`, `timestamp`, `platform`, `environment`, `release`, `level`,\n`message`, `exception_type`, `stacktrace`, `request`, `user_hash`, `tags`, and `extra`. Rotate the per-site token from\nthe dashboard `LA Sentry` tab; tokens are shown only once and are intended for server-side clients. The browser harness uses\n`/sentry/browser-errors` without a token. Browser events are accepted only from the site's configured `allowed_origin`\nvalues, are rate-limited by client IP, and store minimized request metadata.\n\nThe `allowed_origin` field for a site may contain multiple origins separated by spaces or commas (for example `https://mprlab.com http://localhost:8080`); widgets, subscribe forms, and pixels will accept requests from any configured origin while still rejecting traffic from unknown sites.\n\nThe `/api/me` response includes a `role` value of `admin` or `user` and an `avatar.url` pointing to the caller's cached\nprofile image (served from `/api/me/avatar`). The dashboard uses this payload to render the account card and determine\nsite scope.\n\nAuthenticated users can create sites. Owners, creators, and global admins can update and delete sites, can add\nper-site team member emails, and can choose whether selected-site traffic reports go only to the manager, the whole site\nteam, or selected team members. Team members can read assigned site data after signing in with the matching Google email,\nbut cannot manage site settings, memberships, or schedules.\n\nDeployments upgraded from versions prior to LA-57 should allow the server startup migration to run once; it backfills any\nsites missing a `creator_email` with `temirov@gmail.com` to preserve creator-based visibility rules. New site creations\nstore the authenticated creator separately from the configured owner mailbox.\n\n## Dashboard (`/app`)\n\nThe Bootstrap front end consumes the APIs above. Features include:\n\n- Account Settings modal with avatar, email, role badge, reports, and inactivity controls\n- Site creation and owner reassignment available to every authenticated user; administrators additionally see all sites\n- Owner/admin editor for site metadata, with per-site team member emails managed from the Admin dashboard section\n- Selected-site traffic report scheduling with recipient selection for only the manager, the whole site team, or checked team members\n- Widget appearance controls that persist the bubble’s accent color, side (left/right), and bottom offset without code changes\n- Feedback table with human-readable timestamps\n- Subscribers panel with per-site subscriber counts, table, CSV export, and a copyable `subscribe.js` snippet\n- Section selector tabs to switch between Feedback, Subscriptions, Traffic, LA Sentry, and manager-only Admin tools\n- Subscriber deletion via a confirmation modal\n- Traffic card with visit and unique visitor counts, recent visits, and a copyable `pixel.js` snippet\n- Real-time favicon refresh notifications delivered through the SSE stream\n- Sign-out button provided by the shared `mpr-ui`/TAuth shell\n- Inactivity prompt appears after the configured delay (defaults to 60 seconds) and logs out automatically after the configured timeout (defaults to 120 seconds) if unanswered\n\nThe dashboard automatically redirects unauthenticated visitors to `/login`.\n\n## Embedding the widget\n\n1. Create a site (admin) and copy the generated `\u003cscript\u003e` tag from the API response.\n2. Embed the script on any page served from one of the site’s configured `allowed_origin` values (you can supply multiple origins separated by spaces or commas). Include the `defer` attribute so the widget loads without blocking the page; the script waits for the body before rendering the UI.\n3. Visitors can open the floating bubble, submit feedback with a valid email or phone plus a message and/or sentiment, and the messages appear under `/api/sites/:id/messages` and\n   in the dashboard.\n\nExample snippet (replace the base URL with your LoopAware deployment and the site identifier with the value returned by the API):\n\n```html\n\u003cscript defer src=\"https://loopaware.mprlab.com/widget.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f\"\u003e\u003c/script\u003e\n```\n\n## Adding mobile feedback\n\nReact Native and Expo apps can use the first-party client under `clients/react-native` to render a native feedback\nbutton on selected screens. Mobile feedback is separate from LA Sentry error capture.\n\n1. Register the native app for the site:\n\n   ```json\n   POST /api/sites/6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f/mobile-apps\n   {\n     \"platform\": \"ios\",\n     \"app_identifier\": \"com.example.app\",\n     \"display_name\": \"Example iOS\"\n   }\n   ```\n\n2. Store the returned public `client_id` in the app configuration.\n3. Render the feedback button on screens where users should be able to comment:\n\n   ```tsx\n   \u003cLoopAwareProvider\n     siteId=\"6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f\"\n     mobileClientId=\"client-id-from-dashboard-api\"\n     apiOrigin=\"https://loopaware.mprlab.com\"\n     app={{\n       platform: \"ios\",\n       applicationId: \"com.example.app\",\n       version: \"1.2.3\",\n       build: \"44\",\n       environment: \"production\",\n     }}\n   \u003e\n     \u003cCheckoutScreen /\u003e\n     \u003cLoopAwareFeedbackButton\n       screen={{ name: \"Checkout\", path: \"/checkout/payment\" }}\n       context={{ step: \"payment\", plan: \"pro\" }}\n     /\u003e\n   \u003c/LoopAwareProvider\u003e\n   ```\n\nThe public `client_id` identifies the app registration; it is not a secret. Mobile submissions validate that the client\nID, platform, and application identifier match the registered site app, then store the supplied screen, app version, and\nbounded context with the feedback message.\n\n## Embedding the subscribe form\n\nEach site exposes a subscribe snippet that renders an email capture form and posts subscriptions to `/public/subscriptions`.\n\n1. In the dashboard, select a site and use the Subscribers panel to copy the subscribe snippet.\n2. Embed the script on pages served from any of the site’s `allowed_origin` entries. The basic form looks like:\n\n   ```html\n   \u003cscript defer src=\"https://loopaware.mprlab.com/subscribe.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f\"\u003e\u003c/script\u003e\n   ```\n\n3. Optional query parameters let you adjust behavior and styling:\n   - `mode=inline` (default) or `mode=bubble` for a floating button.\n   - `accent=#0d6efd` to override the accent color.\n   - `cta=Subscribe` to customize the button text.\n   - `success=You%27re+on+the+list%21` and `error=Please+try+again.` for inline messages.\n   - `name_field=false` to hide the optional name field.\n\nThe form enforces the site’s `allowed_origin` list using request headers and `source_url` and responds with inline success or\nerror messages so visitors never leave the page.\n\n## Embedding the traffic pixel\n\nThe traffic pixel records page visits per site and powers the dashboard Traffic card and top-pages table.\n\n1. In the dashboard, select a site and use the Traffic panel to copy the pixel snippet.\n2. Embed the script on every page served from any of the site’s `allowed_origin` entries:\n\n   ```html\n   \u003cscript defer src=\"https://loopaware.mprlab.com/pixel.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f\"\u003e\u003c/script\u003e\n   ```\n\n3. On load, `pixel.js` sends a beacon to `/public/visits` with the site ID, current URL, referrer, browser timezone,\n   browser locale, viewport, screen resolution, and a stable visitor ID stored in `localStorage`. The server also stores\n   supported edge geo headers from Cloudflare, Vercel, and CloudFront when the deployment provides them, then prefers\n   that location signal over browser timezone and locale hints. Requests from origins outside the site’s `allowed_origin`\n   list are rejected. Traffic from known bot user-agent signatures is stored but excluded from default dashboard totals,\n   top-page rankings, trends, attribution, engagement, devices, and locations.\n\nFor non-JavaScript environments you can fall back to a plain image pixel:\n\n```html\n\u003cimg src=\"https://loopaware.mprlab.com/public/visits?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f\u0026url=https%3A%2F%2Fexample.com%2F\" alt=\"\" width=\"1\" height=\"1\" /\u003e\n```\n\n## Capturing developer errors\n\nServer-side clients should use the protected `/sentry/errors` endpoint with a per-site ingest token. The repository\ncollects first-party client entrypoints under `clients/`:\n\n- Go: `clients/go/lasentry`\n- Python: `clients/python/la_sentry`\n- Browser: `clients/browser` documents the harness served from `web/la-sentry.js`\n\nBrowser pages can use the standalone harness without exposing the server-side token:\n\n```html\n\u003cscript defer src=\"https://loopaware.mprlab.com/la-sentry.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f\u0026environment=production\u0026release=2026.04.24\"\u003e\u003c/script\u003e\n```\n\nThe browser harness installs `window.LASentry.captureError(error, attrs)` and automatically captures uncaught\n`error` and `unhandledrejection` events. It sends sanitized URL/referrer/user-agent metadata, stack frames, tags, and\nexplicit `extra` values supplied by application code.\n\n## Development workflow\n\n```bash\nmake format\nmake lint\nmake test\n```\n\n`make test` runs the Playwright integration suite against `tests/docker-compose.yml`, with test-owned env fixtures under\n`tests/configs/`. That stack builds the API image, serves `web/` via gHTTP, and exercises both UI and `/api/*` flows.\nUse `make test-unit` for Go-only tests and `make test-integration-api` to focus on API specs. Playwright artifacts\n(traces, screenshots, videos) land under `tests/test-results/` on failure. The integration runner tears its compose\nproject down on exit, including failures and signal exits.\n\nUse `make test-live-favicons` when validating customer-site favicon collection against known public websites. That\ntarget performs live network requests and is intentionally outside `make ci` so third-party uptime does not gate normal\ndevelopment.\n\n## Release, Publish, Deploy\n\nUse the deterministic release-to-production sequence:\n\n```bash\nmake release\nmake publish\nmake deploy\n```\n\n`make release` cuts a repository release from `master`: it preflights the default branch,\nrejects dirty worktrees and open PRs into `master`, runs `make ci`, updates\n`CHANGELOG.md`, pushes the release commit, creates the tag, publishes the GitHub Release\nobject, and verifies remote release state. It does not publish Docker images, publish\nPages, or deploy production.\n\n`make publish` publishes the Docker runtime artifact from a clean `master` checkout after\nverifying that a pushed `vMAJOR.MINOR.PATCH` tag points at `HEAD` and rerunning `make ci`.\nIt pushes:\n\n- `ghcr.io/tyemirov/loopaware:latest`\n- `ghcr.io/tyemirov/loopaware:\u003ctag\u003e`\n- `ghcr.io/tyemirov/loopaware:\u003csha\u003e`\n\n`make deploy` reruns `make ci`, then hands `deploy/app.yml` to\n`mprlab-gateway`. Gateway Ansible deploys and verifies the backend first, then\nexecutes the app-owned GitHub Pages workflow resource from the manifest and\nverifies `https://loopaware.mprlab.com/`. This keeps Pages behind the backend\nversion it depends on without splitting the deploy contract between repos. The\nrelease tag is derived from the v* tag at the app repository `HEAD`; operators\ndo not select a revision during deploy.\n\nThe Docker image and Pages workflows are manual dispatch workflows. They do not publish\nautomatically on tag push; the Makefile targets own the release-to-production ordering.\n\n## Docker\n\nThe previous Docker and Compose files remain compatible. Ensure the container receives the OAuth environment variables\nand mounts `configs/config.loopaware.yml` containing the admin roster.\n\n```bash\ncp configs/.env.loopaware.example configs/.env.loopaware\ncp configs/.env.tauth.example configs/.env.tauth\ncp configs/.env.pinguin.example configs/.env.pinguin\n$EDITOR configs/.env.loopaware configs/.env.tauth configs/.env.pinguin\n./scripts/up.sh\n```\n\nThe compose file binds `configs/config.loopaware.yml` into the LoopAware container at `/app/configs/config.loopaware.yml`\nand loads per-service environment variables via `env_file` from `configs/.env.*`.\nThe container now runs as root so the SQLite data volume remains writable; if you need to switch back to an unprivileged\nuser, update the Docker image to chown the mounted directory before starting the binary.\n\nFor the computercat TLS stack, use:\n\n```bash\n./scripts/up.sh computercat\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftyemirov%2Floopaware","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftyemirov%2Floopaware","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftyemirov%2Floopaware/lists"}