Merge main into go-int/work

# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-internet/board/board/index.html
#	public/dx-maps/gospel-internet/board/index.html
#	public/dx-maps/gospel-internet/board/india-china/index.html
#	public/dx-maps/gospel-internet/board/language-tree/index.html
#	public/dx-maps/gospel-internet/board/platforms/index.html
#	public/dx-maps/gospel-internet/board/project-700/index.html
#	public/dx-maps/gospel-internet/board/zero/index.html
#	public/dx-maps/gospel-internet/gospel-internet.js
#	public/dx-maps/manifest.json
This commit is contained in:
Doxa Maps 2026-09-27 08:31:06 +00:00
commit 7332ffc377
127 changed files with 4284 additions and 957 deletions

3
.gitignore vendored
View file

@ -145,3 +145,6 @@ data-private/
/public/assets/
/public/__gallery/
/public/index.html
# Local plugin backends (backend/<name>/) — the compiled binaries are rebuilt from source, never committed.
/backend/bin/

View file

@ -616,6 +616,14 @@ The studio is a plugin like any other: `plugin-designs/plugin-studio/plugin-stud
| `bun run gates:pre` · `bun run gates:post` | The checks that keep `main` working |
| `bun run shots` | Retake these screenshots (`ONLY=<name>` for one) |
### Run it in the cloud
**One shared dev server for the whole team, no laptop setup.** A container clones the repo, follows
`main`, and hot-reloads every change an agent lands, so everyone sees the Plugin Studio update in
their browser within seconds, with no rebuild. Add OIDC settings and it puts a "Sign in with *your
provider*" page in front of everything, open only to the groups you allow. Recipe and settings:
[deployment/cloud/](./deployment/cloud/README.md).
---
## Repository layout

View file

@ -0,0 +1,456 @@
// devices.go — microphone discovery, hot-plug, and the sound-server environment.
//
// The Jitsi Meet pattern (lib-jitsi-meet modules/RTC/RTCUtils.js), done natively because Rudder records
// outside the browser:
//
// Jitsi (browser) Rudder (native, PipeWire/PulseAudio)
// navigator.mediaDevices.enumerateDevices() → pactl -f json list sources
// 'devicechange' event + polling fallback → pactl subscribe (+ re-list on every source/card event)
// compareAvailableMediaDevices(new) → sameDevices(old, new) — only emit on a real change
// getUserMedia({audio:{deviceId:{exact:id}}}) → parec --device=<id>
//
// Jitsi also reports the device list to analytics (sendDeviceListToAnalytics). Rudder never does:
// the list goes to the local plugin over SSE and nowhere else.
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"math"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
)
// audioEnv gives child processes a path to the user's sound server even when the engine was launched
// from a context without a desktop session (a detached shell, a service, an agent's terminal). Measured
// 2026-09-27: started with no XDG_RUNTIME_DIR, parec exited instantly with "Connection refused" and every
// recording captured nothing, although the socket sat at /run/user/<uid>/pulse/native all along.
func audioEnv() []string {
env := os.Environ()
rt := os.Getenv("XDG_RUNTIME_DIR")
if rt == "" {
rt = fmt.Sprintf("/run/user/%d", os.Getuid())
if st, err := os.Stat(rt); err == nil && st.IsDir() {
env = append(env, "XDG_RUNTIME_DIR="+rt)
}
}
// systemctl --user (a headset-reset command may restart PipeWire) needs the session bus.
if os.Getenv("DBUS_SESSION_BUS_ADDRESS") == "" {
if _, err := os.Stat(rt + "/bus"); err == nil {
env = append(env, "DBUS_SESSION_BUS_ADDRESS=unix:path="+rt+"/bus")
}
}
if os.Getenv("PULSE_SERVER") == "" {
if _, err := os.Stat(rt + "/pulse/native"); err == nil {
env = append(env, "PULSE_SERVER=unix:"+rt+"/pulse/native")
}
}
return env
}
// Device is one microphone as the plugin shows it.
type Device struct {
ID string `json:"id"` // the sound server's source name: what parec --device takes
Label string `json:"label"` // human name ("My Headset", "Built-in Audio Analog Stereo")
Bus string `json:"bus"` // bluetooth | usb | pci | ...
Default bool `json:"default"` // the system's default input
Muted bool `json:"muted"`
Volume string `json:"volume"` // e.g. "52%"
State string `json:"state"` // RUNNING | IDLE | SUSPENDED
}
// DeviceList is the whole picture: the mics, plus sound cards that have a mic but are switched off
// (the laptop's built-in card, profile "off", is the common case — its mic simply doesn't exist until
// the card is turned on in Sound settings).
type DeviceList struct {
Devices []Device `json:"devices"`
Default string `json:"default"`
OffCards []string `json:"offCards"`
Error string `json:"error,omitempty"`
}
func runPactl(args ...string) ([]byte, error) {
cmd := exec.Command("pactl", args...)
cmd.Env = audioEnv()
return cmd.Output()
}
func listDevices() DeviceList {
out := DeviceList{Devices: []Device{}, OffCards: []string{}}
if _, err := exec.LookPath("pactl"); err != nil {
out.Error = "pactl not found: install pipewire-pulse or pulseaudio-utils to list microphones"
return out
}
if b, err := runPactl("get-default-source"); err == nil {
out.Default = strings.TrimSpace(string(b))
}
b, err := runPactl("-f", "json", "list", "sources")
if err != nil {
out.Error = "cannot reach the sound server (pactl list sources failed: " + err.Error() + ")"
return out
}
var raw []struct {
Name string `json:"name"`
Description string `json:"description"`
State string `json:"state"`
Mute bool `json:"mute"`
Volume map[string]any `json:"volume"`
Properties map[string]any `json:"properties"`
}
if err := json.Unmarshal(b, &raw); err != nil {
out.Error = "unexpected pactl output: " + err.Error()
return out
}
for _, s := range raw {
// A ".monitor" source is the loopback of a SPEAKER, not a microphone: listing it would let the
// user "record" their own music. Hidden, as Jitsi lists only audioinput kinds.
if strings.HasSuffix(s.Name, ".monitor") || s.Properties["device.class"] == "monitor" {
continue
}
d := Device{ID: s.Name, Label: s.Description, State: s.State, Muted: s.Mute, Default: s.Name == out.Default}
if bus, ok := s.Properties["device.bus"].(string); ok {
d.Bus = bus
}
for _, ch := range s.Volume { // first channel is representative
if m, ok := ch.(map[string]any); ok {
if p, ok := m["value_percent"].(string); ok {
d.Volume = p
break
}
}
}
out.Devices = append(out.Devices, d)
}
// Cards whose active profile is "off" but which offer an input profile.
if b, err := runPactl("-f", "json", "list", "cards"); err == nil {
var cards []struct {
Name string `json:"name"`
ActiveProfile string `json:"active_profile"`
Properties map[string]any `json:"properties"`
Profiles map[string]struct {
Sources int `json:"sources"`
Available bool `json:"available"`
} `json:"profiles"`
}
if json.Unmarshal(b, &cards) == nil {
for _, c := range cards {
if c.ActiveProfile != "off" {
continue
}
for _, p := range c.Profiles {
if p.Sources > 0 && p.Available {
label, _ := c.Properties["device.description"].(string)
if label == "" {
label = c.Name
}
out.OffCards = append(out.OffCards, label)
break
}
}
}
}
}
return out
}
// sameDevices mirrors Jitsi's compareAvailableMediaDevices: emit only when something a user would
// notice changed (a mic came, went, was muted, or the default moved), not on every state flicker.
func sameDevices(a, b DeviceList) bool {
if a.Default != b.Default || len(a.Devices) != len(b.Devices) || a.Error != b.Error || len(a.OffCards) != len(b.OffCards) {
return false
}
for i := range a.Devices {
x, y := a.Devices[i], b.Devices[i]
if x.ID != y.ID || x.Label != y.Label || x.Muted != y.Muted || x.Volume != y.Volume {
return false
}
}
return true
}
// deviceWatcher keeps the current list and pushes a "devices" event on every real change. It listens
// to `pactl subscribe` (the native 'devicechange') and, like Jitsi, also polls as a fallback in case
// the subscription dies.
type deviceWatcher struct {
mu sync.Mutex
last DeviceList
hub *hub
logs *ring
}
func (w *deviceWatcher) current() DeviceList {
w.mu.Lock()
defer w.mu.Unlock()
return w.last
}
func (w *deviceWatcher) refresh(reason string) {
now := listDevices()
w.mu.Lock()
changed := !sameDevices(w.last, now)
w.last = now
w.mu.Unlock()
if changed {
w.logs.add("microphones (%s): %d found, default=%s %s", reason, len(now.Devices), now.Default, now.Error)
w.hub.send(event{"type": "devices", "devices": now})
}
}
func (w *deviceWatcher) run() {
w.refresh("startup")
// Poll fallback (Jitsi polls enumerateDevices when devicechange is unavailable).
go func() {
for range time.Tick(5 * time.Second) {
w.refresh("poll")
}
}()
go func() {
for {
cmd := exec.Command("pactl", "subscribe")
cmd.Env = audioEnv()
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
out, err := cmd.StdoutPipe()
if err == nil && cmd.Start() == nil {
sc := bufio.NewScanner(out)
var debounce *time.Timer
for sc.Scan() {
l := sc.Text()
if strings.Contains(l, " card ") || (strings.Contains(l, " source ") && !strings.Contains(l, "source-output")) {
w.logs.add("sound-server event: %s", l)
}
if strings.Contains(l, " source ") || strings.Contains(l, " card ") || strings.Contains(l, " server ") {
if debounce != nil {
debounce.Stop()
}
debounce = time.AfterFunc(300*time.Millisecond, func() { w.refresh("hot-plug") })
}
}
_ = cmd.Wait()
}
time.Sleep(3 * time.Second) // sound server restarted or unreachable: retry
}
}()
}
// micTest records ~3 s from one device WITHOUT transcribing and says, in plain words, whether the mic
// works — Rudder's version of Jitsi's pre-join audio preview. Level events stream to the plugin while
// it runs so the meter moves.
func (e *engine) micTest(device string) event {
cmds := recorderCmds(device)
var cmd *exec.Cmd
for _, c := range cmds {
if _, err := exec.LookPath(c[0]); err == nil {
cmd = exec.Command(c[0], c[1:]...)
break
}
}
if cmd == nil {
return event{"ok": false, "verdict": "No recorder found (need parec, pw-record or arecord)."}
}
cmd.Env = audioEnv()
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
var errBuf strings.Builder
cmd.Stderr = &errBuf
out, err := cmd.StdoutPipe()
if err != nil || cmd.Start() != nil {
return event{"ok": false, "verdict": "Could not start the recorder."}
}
defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }()
frame := make([]byte, 3200) // 100 ms
peak, frames, zeroFrames := 0, 0, 0
var sumSq float64
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
n, err := io.ReadFull(out, frame)
if err != nil {
why := strings.TrimSpace(errBuf.String())
e.logs.add("mic test on %q failed after %d frames: %v | %s", device, frames, err, why)
return event{"ok": false, "verdict": "The microphone produced no audio. " + why, "recorder": cmd.Path}
}
fp := 0
for i := 0; i+1 < n; i += 2 {
v := int(int16(uint16(frame[i]) | uint16(frame[i+1])<<8))
sumSq += float64(v * v)
if v < 0 {
v = -v
}
if v > fp {
fp = v
}
}
if fp == 0 {
zeroFrames++
}
if fp > peak {
peak = fp
}
frames++
e.hub.send(event{"type": "level", "peak": float64(fp) / 32768, "test": true})
}
rms := 0.0
if frames > 0 {
rms = math.Sqrt(sumSq/float64(frames*1600)) / 32768
}
res := event{"ok": true, "peak": float64(peak) / 32768, "rms": rms, "frames": frames}
switch {
case zeroFrames == frames:
res["ok"] = false
res["verdict"] = "Pure silence — the mic is connected but sends nothing. It may be muted, or a Bluetooth headset's mic link may have dropped (try reconnecting the headset or pick another mic)."
case peak < 330: // below about -40 dBFS
res["verdict"] = "Very quiet — Rudder can hear the mic, but barely. Speak closer or raise its input volume."
default:
res["verdict"] = "Working — Rudder can hear you."
}
e.logs.add("mic test on %q: peak=%d rms=%.4f frames=%d verdict=%s", device, peak, rms, frames, res["verdict"])
return res
}
// ─── Optional Bluetooth headset reset ─────────────────────────────────────────────────────────────
//
// Some Bluetooth headsets drop the mic side of the HFP link (exact-zero audio while everything looks
// connected; the kernel logs `hci0: corrupted SCO packet`). Re-opening the recorder does not revive
// it; a full reset (disconnect → restart PipeWire/WirePlumber → reconnect) can. That is machine-specific,
// so Rudder only runs one if RUDDER_BT_RESET names an executable that does it (exit 0 = headset works
// again). Rate-limited: never more than once per 20 s.
func btResetPath() string {
p := os.Getenv("RUDDER_BT_RESET")
if p == "" {
return ""
}
if st, err := os.Stat(p); err == nil && !st.IsDir() && st.Mode()&0o111 != 0 {
return p
}
return ""
}
var (
resetMu sync.Mutex
lastReset time.Time
)
// runHeadsetReset runs the RUDDER_BT_RESET command and reports whether it said the headset works again.
func runHeadsetReset(logs *ring) (ok bool, ran bool) {
path := btResetPath()
if path == "" {
return false, false
}
resetMu.Lock()
if time.Since(lastReset) < 20*time.Second {
resetMu.Unlock()
logs.add("headset reset skipped: last one was %.0fs ago (rate limit 20s)", time.Since(lastReset).Seconds())
return false, false
}
lastReset = time.Now()
resetMu.Unlock()
logs.add("running %s (headset reset + CVSD)", path)
cmd := exec.Command(path)
cmd.Env = audioEnv()
done := make(chan struct{})
var out []byte
var err error
go func() { out, err = cmd.CombinedOutput(); close(done) }()
select {
case <-done:
case <-time.After(60 * time.Second):
_ = cmd.Process.Kill()
logs.add("headset reset timed out after 60s")
return false, true
}
for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if l != "" {
logs.add("[headset reset] %s", l)
}
}
if err != nil {
logs.add("headset reset failed: %v", err)
return false, true
}
return true, true
}
// ─── Bluetooth voice-profile helpers ────────────────────────────────────────────────────────────
// btCard maps a Bluetooth mic (bluez_input.<MAC>.<n>, or "" = the default input) to its card.
func btCard(device string) string {
if device == "" {
if b, err := runPactl("get-default-source"); err == nil {
device = strings.TrimSpace(string(b))
}
}
if !strings.HasPrefix(device, "bluez_input.") {
return ""
}
mac := strings.TrimPrefix(device, "bluez_input.")
if i := strings.LastIndex(mac, "."); i > 0 {
mac = mac[:i]
}
return "bluez_card." + mac
}
func currentProfile(card string) string {
if card == "" {
return "(not bluetooth)"
}
b, err := runPactl("-f", "json", "list", "cards")
if err != nil {
return "?"
}
var cards []struct {
Name string `json:"name"`
ActiveProfile string `json:"active_profile"`
}
if json.Unmarshal(b, &cards) != nil {
return "?"
}
for _, c := range cards {
if c.Name == card {
return c.ActiveProfile
}
}
return "?"
}
// switchProfile forces a fresh voice link: CVSD if the card is on anything else, otherwise a bounce
// through mSBC and back to CVSD (a same-profile set is a no-op and would not renegotiate the link).
func switchProfile(card string, logs *ring) error {
before := currentProfile(card)
target := "headset-head-unit-cvsd"
if before == target {
if _, err := runPactl("set-card-profile", card, "headset-head-unit-msbc"); err != nil {
logs.add("profile bounce via msbc failed: %v", err)
}
time.Sleep(700 * time.Millisecond)
}
if out, err := runPactl("set-card-profile", card, target); err != nil {
return fmt.Errorf("pactl set-card-profile %s %s: %v %s", card, target, err, strings.TrimSpace(string(out)))
}
time.Sleep(1500 * time.Millisecond) // let the new SCO link and the source come up
logs.add("voice profile %s → %s", before, currentProfile(card))
return nil
}
// watchKernelBluetooth copies kernel Bluetooth messages into the log while a session runs
// (`journalctl -k` is readable without root on this system).
func watchKernelBluetooth(ctx context.Context, logs *ring) {
cmd := exec.CommandContext(ctx, "journalctl", "-k", "-f", "-n", "0", "-o", "cat")
out, err := cmd.StdoutPipe()
if err != nil || cmd.Start() != nil {
return
}
sc := bufio.NewScanner(out)
for sc.Scan() {
if l := sc.Text(); strings.Contains(strings.ToLower(l), "bluetooth") {
logs.add("kernel: %s", l)
}
}
_ = cmd.Wait()
}

View file

@ -0,0 +1,5 @@
module dx-maps/backend/rudder-engine
go 1.22.2
require github.com/gorilla/websocket v1.5.3

View file

@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

View file

@ -0,0 +1,797 @@
// rudder-engine — the local speech backend for the rudder-voice plugin.
//
// WHY THIS EXISTS: rudder-voice is a DESKTOP-target plugin. Speech-to-text runs on the user's own
// machine, never in the browser and never on a server. In dev, `bun run dev` starts this process and
// Vite forwards /api/rudder/* to it; in the desktop app, Tauri runs it as a sidecar. Same binary,
// same API, so "works in the Plugin Studio" means "works on desktop".
//
// browser / webview ──HTTP+SSE──► rudder-engine (127.0.0.1:RUDDER_PORT)
// ├─ recorder: parec → pw-record → arecord (mic open ONLY while recording)
// └─ WebSocket ──► nemo-speech serve (127.0.0.1:RUDDER_NEMO_PORT)
// NVIDIA NeMo-Speech.cpp + Nemotron 3.5 ASR GGUF
//
// NO TELEMETRY, NO NETWORK: the runtime is NVIDIA NeMo-Speech.cpp (ggml). ONNX-based runtimes were
// REJECTED: measured 2026-09-26, the tested ONNX GenAI runtime looked up a vendor telemetry host even
// with telemetry "disabled". NeMo-Speech.cpp was traced with strace: zero sockets, zero DNS.
// Its only network path is an optional model download that shells out to `curl` when the model is not
// a local file — so this engine REFUSES to start unless RUDDER_MODEL is an existing local file.
package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/gorilla/websocket"
)
// ─── Config (env, with local defaults) ──────────────────────────────────────────────────────────
type config struct {
Port string // this engine's HTTP port
NemoPort string // the warm NeMo-Speech server's port (loopback only)
NemoBin string // path to the nemo-speech executable
Model string // local GGUF path — MUST exist (see the no-network note above)
Language string // default language code
EndpointMs int // silence that ends an utterance
}
func envOr(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
}
return d
}
func loadConfig() config {
home, _ := os.UserHomeDir()
base := filepath.Join(home, ".local", "share", "rudder")
return config{
Port: envOr("RUDDER_PORT", "5193"),
NemoPort: envOr("RUDDER_NEMO_PORT", "5194"),
NemoBin: envOr("RUDDER_NEMO_BIN", filepath.Join(base, "bin", "nemo-speech-0.1.0-linux-x86_64-cpu", "bin", "nemo-speech")),
Model: envOr("RUDDER_MODEL", filepath.Join(base, "models", "nemotron-3.5-asr-streaming-0.6b.q8_0.gguf")),
Language: envOr("RUDDER_LANG", "en-US"),
EndpointMs: 800,
}
}
// ─── Log ring: every line is also served at /api/rudder/log so the Debug panel can copy it ──────
type ring struct {
mu sync.Mutex
lines []string
}
// ★ PRIVACY: the log is what people paste to an agent (🐞 Debug → copy report), so it must never carry
// anything that identifies them. Every line is redacted as it is written: device MAC addresses (inside
// Bluetooth device names like bluez_input.AA_BB_…) and the home folder (which contains the username).
// Transcribed words are never logged at all.
var (
macRe = regexp.MustCompile(`(?i)\b([0-9a-f]{2}[:_]){5}[0-9a-f]{2}\b`)
homeDir = func() string { h, _ := os.UserHomeDir(); return h }()
)
func redact(s string) string {
s = macRe.ReplaceAllString(s, "XX:XX:XX:XX:XX:XX")
if homeDir != "" && homeDir != "/" {
s = strings.ReplaceAll(s, homeDir, "~")
}
return s
}
func (r *ring) add(format string, a ...any) {
line := redact(time.Now().Format("15:04:05.000") + " " + fmt.Sprintf(format, a...))
log.Println(line)
r.mu.Lock()
r.lines = append(r.lines, line)
if len(r.lines) > 3000 {
r.lines = r.lines[len(r.lines)-3000:]
}
r.mu.Unlock()
}
func (r *ring) text() string {
r.mu.Lock()
defer r.mu.Unlock()
return strings.Join(r.lines, "\n")
}
// ─── Event hub: Server-Sent Events to every open plugin ─────────────────────────────────────────
type event map[string]any
type hub struct {
mu sync.Mutex
clients map[chan []byte]struct{}
}
func (h *hub) subscribe() chan []byte {
c := make(chan []byte, 64)
h.mu.Lock()
h.clients[c] = struct{}{}
h.mu.Unlock()
return c
}
func (h *hub) unsubscribe(c chan []byte) {
h.mu.Lock()
delete(h.clients, c)
h.mu.Unlock()
}
func (h *hub) send(e event) {
b, _ := json.Marshal(e)
h.mu.Lock()
defer h.mu.Unlock()
for c := range h.clients {
select {
case c <- b:
default: // a slow client drops an event rather than stalling the audio path
}
}
}
// ─── Engine: supervises nemo-speech serve and one recording session at a time ───────────────────
type engine struct {
cfg config
logs *ring
hub *hub
mu sync.Mutex
state string // starting | ready | error
stateErr string
nemo *exec.Cmd
recording bool
recorder string
language string
stopSession func()
device string
devs *deviceWatcher
nemoKey string
}
func (e *engine) setState(s, errText string) {
e.mu.Lock()
e.state, e.stateErr = s, errText
e.mu.Unlock()
e.hub.send(event{"type": "status", "engine": s, "error": errText})
}
func (e *engine) status() event {
e.mu.Lock()
defer e.mu.Unlock()
return event{
"engine": e.state, "error": e.stateErr, "recording": e.recording, "recorder": e.recorder,
"language": e.language, "defaultLanguage": e.cfg.Language, "device": e.device,
"runtime": "NVIDIA NeMo-Speech.cpp (ggml, CPU)", "model": filepath.Base(e.cfg.Model),
}
}
// startNemo launches the warm speech server. The model loads and warms up once (~20 s on CPU),
// so starting and stopping a recording is instant afterwards.
func (e *engine) startNemo() {
if st, err := os.Stat(e.cfg.Model); err != nil || st.IsDir() {
e.setState("error", "model file not found: "+e.cfg.Model+" (refusing to start: a non-local model would make the runtime download it)")
return
}
if _, err := os.Stat(e.cfg.NemoBin); err != nil {
e.setState("error", "nemo-speech runtime not found: "+e.cfg.NemoBin)
return
}
e.setState("starting", "")
// A fresh random key each start: the speech server answers only this engine, not any other local
// program or web page that finds its port.
cmd := exec.Command(e.cfg.NemoBin, "serve",
"--host", "127.0.0.1", "--port", e.cfg.NemoPort,
"--asr-model", e.cfg.Model, "--device", "cpu", "--no-ui", "--api-key", e.nemoKey)
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} // dies with us
out, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
e.setState("error", "could not start nemo-speech: "+err.Error())
return
}
e.mu.Lock()
e.nemo = cmd
e.mu.Unlock()
e.logs.add("nemo-speech serve started (pid %d) on 127.0.0.1:%s", cmd.Process.Pid, e.cfg.NemoPort)
go func() {
buf := make([]byte, 4096)
for {
n, err := out.Read(buf)
for _, l := range strings.Split(strings.TrimSpace(string(buf[:n])), "\n") {
if l != "" {
e.logs.add("[nemo] %s", l)
}
}
if err != nil {
return
}
}
}()
go func() {
err := cmd.Wait()
e.setState("error", fmt.Sprintf("nemo-speech exited: %v", err))
}()
// Ready = the model list answers.
go func() {
url := "http://127.0.0.1:" + e.cfg.NemoPort + "/v1/models"
for i := 0; i < 600; i++ {
time.Sleep(500 * time.Millisecond)
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Authorization", "Bearer "+e.nemoKey)
resp, err := http.DefaultClient.Do(req)
if err == nil {
resp.Body.Close()
if resp.StatusCode == 200 {
e.logs.add("nemo-speech ready after %.1fs", float64(i+1)*0.5)
e.setState("ready", "")
return
}
}
e.mu.Lock()
s := e.state
e.mu.Unlock()
if s == "error" {
return
}
}
e.setState("error", "nemo-speech did not become ready within 5 minutes")
}()
}
// recorderCmds: raw s16le 16 kHz mono PCM on stdout. parec first — on PipeWire/Bluetooth desktops it
// talks to the sound server directly (the old Rudder's hardest-won lesson: PortAudio/ALSA-only
// capture stalls on Bluetooth headsets).
// RUDDER_RECORDER overrides the list with one space-separated command (e.g. a test that replays a
// PCM file), so the engine can be tested end to end without a microphone.
// device is a sound-server source name from /api/rudder/devices; empty = the system default input.
func recorderCmds(device string) [][]string {
if v := os.Getenv("RUDDER_RECORDER"); v != "" {
return [][]string{strings.Fields(v)}
}
parec := []string{"parec", "--format=s16le", "--rate=16000", "--channels=1", "--raw", "--latency-msec=100"}
pw := []string{"pw-record", "--format=s16", "--rate=16000", "--channels=1"}
if device != "" {
parec = append(parec, "--device="+device)
pw = append(pw, "--target="+device)
}
pw = append(pw, "-")
cmds := [][]string{parec, pw}
if device == "" { // arecord can't address a sound-server source by name
cmds = append(cmds, []string{"arecord", "-q", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw"})
}
return cmds
}
func (e *engine) start(lang, device string) error {
e.mu.Lock()
if e.state != "ready" {
e.mu.Unlock()
return fmt.Errorf("engine is %s — wait for it to be ready", e.state)
}
if e.recording {
e.mu.Unlock()
return nil
}
if lang == "" {
lang = e.cfg.Language
}
e.mu.Unlock()
ws, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:"+e.cfg.NemoPort+"/v1/audio/transcriptions/realtime",
http.Header{"Authorization": {"Bearer " + e.nemoKey}})
if err != nil {
return fmt.Errorf("connect to speech server: %w", err)
}
upd, _ := json.Marshal(event{"type": "session.update", "session": event{
"sample_rate": 16000, "language": lang, "automatic_punctuation": true, "endpointing_ms": e.cfg.EndpointMs,
}})
if err := ws.WriteMessage(websocket.TextMessage, upd); err != nil {
ws.Close()
return fmt.Errorf("session.update: %w", err)
}
// Bluetooth headsets start on CVSD, not mSBC. Measured 2026-09-27 on a Bluetooth headset: on mSBC the
// mic link died 3-5 s into EVERY session that had speech (kernel: corrupted SCO packets); after
// switching to CVSD it ran 9.5 minutes without a drop. CVSD is 8 kHz, and Nemotron's transcript of an
// 8 kHz copy of the test clip differed by one word. RUDDER_KEEP_MSBC=1 opts out.
if card := btCard(device); card != "" && os.Getenv("RUDDER_KEEP_MSBC") == "" {
if p := currentProfile(card); p == "headset-head-unit-msbc" {
e.logs.add("bluetooth headset on mSBC — switching to CVSD before recording (stable voice link)")
e.hub.send(event{"type": "recovering", "message": "Switching your Bluetooth headset to its stable voice mode…"})
if err := switchProfile(card, e.logs); err != nil {
e.logs.add("pre-switch to CVSD failed: %v", err)
} else {
e.hub.send(event{"type": "recovered", "message": "Headset is in its stable voice mode — go ahead and talk."})
}
}
}
first, err := spawnRecorder(device, e.logs)
if err != nil {
ws.Close()
return err
}
recName := first.name
var recMu sync.Mutex
cur := first // the live recorder; replaced when a dead mic link is recovered
ctx, cancel := context.WithCancel(context.Background())
var writeMu sync.Mutex
done := make(chan struct{})
e.mu.Lock()
e.recording, e.recorder, e.language, e.device = true, recName, lang, device
e.stopSession = func() {
cancel()
recMu.Lock()
cur.kill()
recMu.Unlock()
writeMu.Lock()
_ = ws.WriteMessage(websocket.TextMessage, []byte(`{"type":"input_audio_buffer.commit"}`))
writeMu.Unlock()
select { // let the final words arrive
case <-done:
case <-time.After(4 * time.Second):
}
ws.Close()
}
e.mu.Unlock()
e.logs.add("recording started: recorder=%s device=%q language=%s profile=%s", recName, device, lang, currentProfile(btCard(device)))
go watchKernelBluetooth(ctx, e.logs)
e.hub.send(event{"type": "recording", "recording": true, "recorder": recName, "language": lang, "device": device})
for _, d := range e.devs.current().Devices {
if (d.ID == device || (device == "" && d.Default)) && d.Muted {
e.hub.send(event{"type": "warning", "message": "Your microphone \"" + d.Label + "\" is muted in the system sound settings — unmute it or nothing will be heard."})
}
}
// Audio pump: 100 ms frames → speech server, plus a level meter and dead-mic RECOVERY.
go func() {
frame := make([]byte, 3200)
var silentSince time.Time
recoveries := 0
var recovering atomic.Bool
var resetting atomic.Bool
tracePeak, lastTrace := 0, time.Now()
var healthySince time.Time // a mic that stays healthy 30 s earns back its 3 reconnects
gaveUp := false
lastLevel := time.Time{}
for {
recMu.Lock()
r := cur
recMu.Unlock()
if _, err := io.ReadFull(r.out, frame); err != nil {
if ctx.Err() != nil {
return
}
recMu.Lock()
swapped := cur != r
recMu.Unlock()
if swapped { // we killed it ourselves during a recovery: read from the new one
continue
}
// A headset reset restarts PipeWire, which kills parec mid-read. That is expected:
// wait for the reset to hand over a fresh recorder instead of ending the session.
if resetting.Load() {
for resetting.Load() && ctx.Err() == nil {
time.Sleep(200 * time.Millisecond)
}
continue
}
// The recorder died on its own (bad device, sound server unreachable, device unplugged).
// End the session so the UI stops showing "Listening", and say WHY in plain words.
_ = r.cmd.Wait()
why := strings.TrimSpace(r.errBuf.String())
e.logs.add("recorder %s ended unexpectedly: %v | stderr: %s", r.name, err, why)
msg := "The microphone stopped delivering audio."
switch {
case strings.Contains(why, "Connection refused") || strings.Contains(why, "pa_context_connect"):
msg = "Can't reach the computer's sound system (PipeWire/PulseAudio). Is it running?"
case strings.Contains(why, "No such entity") || strings.Contains(why, "not found"):
msg = "That microphone isn't available any more — it may have been unplugged or disconnected. Pick another one."
}
if why != "" {
msg += " (" + r.name + ": " + why + ")"
}
e.hub.send(event{"type": "error", "message": msg})
go e.stop()
return
}
peak := 0
for i := 0; i+1 < len(frame); i += 2 {
v := int(int16(binary.LittleEndian.Uint16(frame[i:])))
if v < 0 {
v = -v
}
if v > peak {
peak = v
}
}
// ★ DEAD MIC LINK. Exact-zero samples are not quiet speech (a live mic always has a noise
// floor). Measured 2026-09-27 on a Bluetooth headset (HFP, mSBC): four words
// transcribed, then literal zeros while parec stayed "healthy" — the old Rudder's BUG 1,
// the headset losing the mic side of the link. Re-opening the source restarts the audio
// stream; the speech session and the transcript carry on untouched.
if peak == 0 {
if silentSince.IsZero() {
silentSince = time.Now()
} else if !gaveUp && !resetting.Load() && time.Since(silentSince) > 1500*time.Millisecond {
recoveries++
switch {
case recoveries == 1:
// Step 1: reopen the recorder (cheap; fixes a stalled stream).
e.logs.add("mic link dead (exact zeros 1.5s) — step 1: reopening the microphone")
e.hub.send(event{"type": "recovering", "attempt": recoveries, "message": "Microphone went silent — reconnecting it…"})
if next, err := spawnRecorder(device, e.logs); err == nil {
recMu.Lock()
old := cur
cur = next
recMu.Unlock()
old.kill()
recovering.Store(true)
}
case recoveries == 2 && btCard(device) != "":
// Step 2: force a NEW Bluetooth voice link by switching the headset's voice profile to
// CVSD (the old Rudder's proven-stabler choice). Unlike a detect-first reset script this does not
// "detect first", so it can't decide a dead link is fine.
card := btCard(device)
e.logs.add("mic still dead — step 2: forcing a new voice link: %s → headset-head-unit-cvsd", card)
e.hub.send(event{"type": "recovering", "attempt": recoveries, "message": "Microphone still silent — switching your Bluetooth headset to its more stable voice mode…"})
resetting.Store(true)
go func() {
defer resetting.Store(false)
if err := switchProfile(card, e.logs); err != nil {
e.hub.send(event{"type": "warning", "message": "Couldn't switch the headset's voice mode: " + err.Error()})
return
}
if next, err := spawnRecorder(device, e.logs); err == nil {
recMu.Lock()
old := cur
cur = next
recMu.Unlock()
old.kill()
recovering.Store(true)
}
}()
case recoveries == 3 && btResetPath() != "":
// Step 3: full headset reset (disconnect → restart PipeWire → reconnect).
e.logs.add("mic still dead — step 3: full headset reset via RUDDER_BT_RESET")
e.hub.send(event{"type": "recovering", "attempt": recoveries, "message": "Still silent — fully resetting your Bluetooth headset (about 15 seconds)…"})
resetting.Store(true)
go func() {
defer resetting.Store(false)
ok, ran := runHeadsetReset(e.logs)
if !ran {
return
}
if !ok {
e.hub.send(event{"type": "warning", "message": "The headset reset didn't bring the mic back — is the headset on and in range?"})
}
if next, err := spawnRecorder(device, e.logs); err == nil {
recMu.Lock()
old := cur
cur = next
recMu.Unlock()
old.kill()
recovering.Store(true)
}
}()
default:
gaveUp = true
e.logs.add("mic still dead after reopen + headset resets — giving up for this session")
msg := "Your microphone keeps going silent. This is the Bluetooth headset dropping its mic link (the kernel reports corrupted voice packets), not Rudder. Try a wired or built-in mic, or turn off the headset's noise cancellation / adaptive audio in its app."
if btResetPath() == "" {
msg += " (No headset-reset command is configured — see RUDDER_BT_RESET.)"
}
e.hub.send(event{"type": "warning", "message": msg})
}
silentSince = time.Now()
}
} else {
if recovering.Load() {
recovering.Store(false)
healthySince = time.Now()
e.logs.add("mic recovered after %d reconnect(s)", recoveries)
e.hub.send(event{"type": "recovered", "message": "Microphone is back — keep talking."})
}
if recoveries > 0 && !healthySince.IsZero() && time.Since(healthySince) > 30*time.Second {
recoveries, gaveUp, healthySince = 0, false, time.Time{}
}
silentSince = time.Time{}
}
// Flight recorder: the mic level twice a second, so a drop can be lined up against kernel and
// sound-server events in the same log (/api/rudder/log).
tracePeak = max(tracePeak, peak)
if time.Since(lastTrace) >= 500*time.Millisecond {
e.logs.add("trace level peak=%d", tracePeak)
tracePeak, lastTrace = 0, time.Now()
}
if time.Since(lastLevel) > 150*time.Millisecond {
lastLevel = time.Now()
e.hub.send(event{"type": "level", "peak": float64(peak) / 32768})
}
writeMu.Lock()
err := ws.WriteMessage(websocket.BinaryMessage, frame)
writeMu.Unlock()
if err != nil {
if ctx.Err() == nil {
e.hub.send(event{"type": "error", "message": "speech server connection lost: " + err.Error()})
}
return
}
}
}()
// Result reader: map the server's events onto the plugin's small vocabulary.
go func() {
defer close(done)
for {
_, msg, err := ws.ReadMessage()
if err != nil {
return
}
var m map[string]any
if json.Unmarshal(msg, &m) != nil {
continue
}
t, _ := m["type"].(string)
text := firstString(m, "transcript", "delta", "text")
switch {
case strings.HasSuffix(t, "input_audio_transcription.delta"):
// Deltas APPEND to the utterance in progress; the server sends many empty ones.
if text != "" {
e.hub.send(event{"type": "partial", "text": text})
}
case strings.HasSuffix(t, "input_audio_transcription.completed"):
e.hub.send(event{"type": "final", "text": text, "at": time.Now().Format(time.RFC3339)})
case t == "error":
e.logs.add("speech server error: %s", string(msg))
e.hub.send(event{"type": "error", "message": string(msg)})
case t == "input_audio_buffer.committed":
return
default:
e.logs.add("speech event: %s", t)
}
}
}()
return nil
}
// recorderProc is one running recorder process.
type recorderProc struct {
cmd *exec.Cmd
out io.ReadCloser
name string
errBuf *bytes.Buffer
}
func (r *recorderProc) kill() {
if r != nil && r.cmd != nil && r.cmd.Process != nil {
_ = r.cmd.Process.Kill()
go func() { _ = r.cmd.Wait() }()
}
}
// spawnRecorder starts the first recorder that works for this device.
func spawnRecorder(device string, logs *ring) (*recorderProc, error) {
for _, c := range recorderCmds(device) {
if _, err := exec.LookPath(c[0]); err != nil {
continue
}
cmd := exec.Command(c[0], c[1:]...)
cmd.Env = audioEnv()
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
errBuf := &bytes.Buffer{}
cmd.Stderr = errBuf
out, err := cmd.StdoutPipe()
if err != nil {
continue
}
if err := cmd.Start(); err != nil {
logs.add("recorder %s failed to start: %v", c[0], err)
continue
}
return &recorderProc{cmd: cmd, out: out, name: c[0], errBuf: errBuf}, nil
}
return nil, errors.New("no microphone recorder found (need parec, pw-record or arecord)")
}
func firstString(m map[string]any, keys ...string) string {
for _, k := range keys {
if s, ok := m[k].(string); ok {
return s
}
}
if item, ok := m["item"].(map[string]any); ok {
return firstString(item, keys...)
}
return ""
}
func (e *engine) stop() {
e.mu.Lock()
stop := e.stopSession
e.stopSession = nil
was := e.recording
e.recording = false
e.mu.Unlock()
if !was || stop == nil {
return
}
stop()
e.logs.add("recording stopped")
e.hub.send(event{"type": "recording", "recording": false})
}
func (e *engine) shutdown() {
e.stop()
e.mu.Lock()
n := e.nemo
e.mu.Unlock()
if n != nil && n.Process != nil {
_ = n.Process.Kill()
}
}
// ─── HTTP API ────────────────────────────────────────────────────────────────────────────────────
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
// guard: two protections for a server that streams what you say.
// - Host check (DNS rebinding): a web page on any domain can make the browser resolve its own hostname
// to 127.0.0.1 and then read this API as "same-origin". Only requests addressed to 127.0.0.1 or
// localhost are served.
// - CORS: only the desktop webview's origin (tauri://localhost, or http://tauri.localhost on Windows)
// may read responses cross-origin. The Plugin Studio needs none: it reaches /api/rudder through the
// dev server's same-origin proxy. RUDDER_ALLOW_ORIGIN adds one more exact origin (tests).
func guard(next http.Handler) http.Handler {
extra := os.Getenv("RUDDER_ALLOW_ORIGIN")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
if i := strings.LastIndex(host, ":"); i >= 0 && !strings.HasSuffix(host, "]") {
host = host[:i]
}
if host != "127.0.0.1" && host != "localhost" && host != "[::1]" {
http.Error(w, "forbidden host", http.StatusForbidden)
return
}
o := r.Header.Get("Origin")
if o == "tauri://localhost" || o == "http://tauri.localhost" || (extra != "" && o == extra) {
w.Header().Set("Access-Control-Allow-Origin", o)
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Vary", "Origin")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
cfg := loadConfig()
logs := &ring{}
h := &hub{clients: map[chan []byte]struct{}{}}
key := make([]byte, 24)
if _, err := rand.Read(key); err != nil {
log.Fatal("no randomness for the speech-server key: ", err)
}
e := &engine{cfg: cfg, logs: logs, hub: h, state: "starting", devs: &deviceWatcher{hub: h, logs: logs}, nemoKey: hex.EncodeToString(key)}
e.devs.run()
mux := http.NewServeMux()
mux.HandleFunc("/api/rudder/status", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, e.status())
})
mux.HandleFunc("/api/rudder/start", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, 405, event{"error": "POST only"})
return
}
var body struct {
Language string `json:"language"`
Device string `json:"device"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
if err := e.start(body.Language, body.Device); err != nil {
e.logs.add("start failed: %v", err)
writeJSON(w, 409, event{"error": err.Error()})
return
}
writeJSON(w, 200, e.status())
})
mux.HandleFunc("/api/rudder/stop", func(w http.ResponseWriter, r *http.Request) {
e.stop()
writeJSON(w, 200, e.status())
})
mux.HandleFunc("/api/rudder/devices", func(w http.ResponseWriter, r *http.Request) {
e.devs.refresh("request")
writeJSON(w, 200, e.devs.current())
})
mux.HandleFunc("/api/rudder/test", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Device string `json:"device"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
writeJSON(w, 200, e.micTest(body.Device))
})
mux.HandleFunc("/api/rudder/log", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = io.WriteString(w, logs.text())
})
mux.HandleFunc("/api/rudder/events", func(w http.ResponseWriter, r *http.Request) {
fl, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", 500)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
c := e.hub.subscribe()
defer e.hub.unsubscribe(c)
first, _ := json.Marshal(event{"type": "status", "status": e.status()})
fmt.Fprintf(w, "data: %s\n\n", first)
devs, _ := json.Marshal(event{"type": "devices", "devices": e.devs.current()})
fmt.Fprintf(w, "data: %s\n\n", devs)
fl.Flush()
ping := time.NewTicker(15 * time.Second)
defer ping.Stop()
for {
select {
case <-r.Context().Done():
return
case b := <-c:
fmt.Fprintf(w, "data: %s\n\n", b)
fl.Flush()
case <-ping.C:
fmt.Fprint(w, ": ping\n\n")
fl.Flush()
}
}
})
srv := &http.Server{Addr: "127.0.0.1:" + cfg.Port, Handler: guard(mux)}
logs.add("rudder-engine listening on http://127.0.0.1:%s (runtime=%s model=%s)", cfg.Port, cfg.NemoBin, cfg.Model)
e.startNemo()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sig
e.shutdown()
os.Exit(0)
}()
if err := srv.ListenAndServe(); err != nil {
e.shutdown()
log.Fatal(err)
}
}

View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# setup-runtime.sh — fetch Rudder's speech runtime + model, verified by SHA-256, then build the engine.
#
# Rudder (plugin-designs/rudder-voice) transcribes on THIS computer. It needs two things that are too big
# for git: NVIDIA's NeMo-Speech.cpp runtime (~4 MB, CPU build) and the Nemotron 3.5 ASR Streaming GGUF
# (~742 MB). Both are pinned by version and checksum; a mismatch aborts instead of running an unverified
# file. After this, `bun run dev` starts the engine and Rudder appears in the Plugin Studio.
#
# bash backend/rudder-engine/setup-runtime.sh # installs into ~/.local/share/rudder
# RUDDER_HOME=/some/dir bash backend/rudder-engine/setup-runtime.sh
#
# Linux x86_64 only for now (the runtime ships other platforms; add them when a desktop build needs them).
set -euo pipefail
RUDDER_HOME="${RUDDER_HOME:-$HOME/.local/share/rudder}"
RT_VER="0.1.0"
RT_NAME="nemo-speech-${RT_VER}-linux-x86_64-cpu"
RT_URL="https://github.com/NVIDIA/NeMo-Speech.cpp/releases/download/v${RT_VER}/${RT_NAME}.tar.gz"
RT_SHA="0f74131d631ad2c694cf0ec53490866bb6461147959589a69fb6fc231944065b"
# NVIDIA re-converted the GGUF on 2026-09-10 (embedded tokenizer); this revision is the one Rudder is tested with.
MODEL_REV="ea30d66debe3"
MODEL_FILE="nemotron-3.5-asr-streaming-0.6b.q8_0.gguf"
MODEL_URL="https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b/resolve/${MODEL_REV}/${MODEL_FILE}"
MODEL_SHA="3fc991d3badad7277c11030a7519832cddaf2057aafed6d4b25147e953a070b1"
say() { printf '\033[1;34m→\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
[ "$(uname -s)-$(uname -m)" = "Linux-x86_64" ] || die "This script currently supports Linux x86_64 only."
command -v curl >/dev/null || die "curl is required."
command -v sha256sum >/dev/null || die "sha256sum is required."
fetch() { # url dest sha
local url="$1" dest="$2" sha="$3"
if [ -f "$dest" ] && [ "$(sha256sum "$dest" | cut -d' ' -f1)" = "$sha" ]; then
say "already have $(basename "$dest") (checksum OK)"; return
fi
say "downloading $(basename "$dest")"
curl -fL --retry 3 -C - -o "$dest.part" "$url"
[ "$(sha256sum "$dest.part" | cut -d' ' -f1)" = "$sha" ] || { rm -f "$dest.part"; die "checksum mismatch for $(basename "$dest") — refusing to use it"; }
mv "$dest.part" "$dest"
}
mkdir -p "$RUDDER_HOME/bin" "$RUDDER_HOME/models"
fetch "$RT_URL" "$RUDDER_HOME/bin/$RT_NAME.tar.gz" "$RT_SHA"
tar -xzf "$RUDDER_HOME/bin/$RT_NAME.tar.gz" -C "$RUDDER_HOME/bin"
fetch "$MODEL_URL" "$RUDDER_HOME/models/$MODEL_FILE" "$MODEL_SHA"
if command -v go >/dev/null; then
say "building backend/bin/rudder-engine"
(cd "$(dirname "$0")" && go build -o ../bin/rudder-engine .)
else
printf '! Go not found — install Go, then: cd backend/rudder-engine && go build -o ../bin/rudder-engine .\n'
fi
for tool in parec pactl; do command -v "$tool" >/dev/null || printf '! %s not found — install pipewire-pulse or pulseaudio-utils for microphone capture.\n' "$tool"; done
say "done. Runtime: $RUDDER_HOME/bin/$RT_NAME Model: $RUDDER_HOME/models/$MODEL_FILE"
say "run \`bun run dev\` and open Rudder (rudder-voice) in the Plugin Studio."

View file

@ -0,0 +1,23 @@
{
"_what": "Evangelical share of the DOXA people groups' POPULATION, for the packet's life-calendar page.",
"_method": "Each group's published EvangelicalLevel BAND is taken at its midpoint (0 for 'No Known Evangelicals'), weighted by the group's Population, and summed. peoplegroups.org publishes a band, never a number, so a percentage requires choosing a value inside each band; the midpoint is stated on the page itself.",
"_join": "IMB CSV ROP3Code + ISOAlpha3 against doxa-groups-snapshot.json rop3 + country — the same join the Vision Card uses (build-lists.mjs:13).",
"sources": {
"imb": "data/people-groups/people_groups.csv",
"doxa": "https://pray.doxa.life/api/people-groups/list",
"fetched": "2026-09-25"
},
"groupsInList": 2107,
"groupsMatched": 2091,
"population": 209079159,
"evangelicalPopulation": 1282178,
"pctEvangelical": 0.6133,
"bands": {
"No Known Evangelicals": 1444,
"Less than 2%": 638,
"2% or Greater but Less than 5%": 6,
"5% or Greater but Less than 10%": 1,
"10% or Greater but Less than 15%": 1,
"50% or Greater but Less than 75%": 1
}
}

View file

@ -0,0 +1,21 @@
# dx-maps cloud dev server: one shared Vite dev server with hot reload for a whole team.
# This image is only a bootstrap: at start it clones the repo and runs deployment/cloud/entry.sh
# FROM THE REPO, so the recipe lives in git and updates with it. Paste this file as-is into a
# container host (Coolify "Dockerfile" build pack, Railway, or `docker build`).
FROM quay.io/oauth2-proxy/oauth2-proxy:v7.15.3 AS oauth2proxy
FROM oven/bun:1-debian
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=oauth2proxy /bin/oauth2-proxy /usr/local/bin/oauth2-proxy
RUN printf '%s\n' \
'#!/bin/bash' \
'set -u; export GIT_TERMINAL_PROMPT=0' \
'T="${GIT_TOKEN:-${FORGEJO_TOKEN:-}}"' \
'if [ -n "$T" ]; then printf "#!/bin/sh\ncase \"\$1\" in *sername*) echo \"\$GIT_TOKEN_EFF\";; *) echo;; esac\n" > /tmp/askpass.sh; chmod 700 /tmp/askpass.sh; export GIT_ASKPASS=/tmp/askpass.sh GIT_TOKEN_EFF="$T"; fi' \
': "${REPO_URL:?set REPO_URL to the dx-maps git URL}"' \
'[ -d /app/repo/.git ] || git clone -q --depth 1 --branch "${BRANCH:-main}" "$REPO_URL" /app/repo || { echo "clone failed"; sleep 60; exit 1; }' \
'cp /app/repo/deployment/cloud/entry.sh /tmp/entry.sh && exec bash /tmp/entry.sh' \
> /boot.sh && chmod 755 /boot.sh
ENV PORT=5173 BRANCH=main PULL_INTERVAL=15
EXPOSE 5173
CMD ["/boot.sh"]

View file

@ -0,0 +1,45 @@
# Run dx-maps in the cloud: one shared dev server for a team
One container runs the Plugin Studio with hot reload for everyone. It clones the repo, pulls the
branch every few seconds, and Vite hot-reloads whatever changed, so every change an agent lands
on `main` shows up in everyone's browser within seconds. No rebuild; `bun run build` is only for
publishing.
With **no OIDC settings** it runs open, like `bun run dev` on a laptop. With **OIDC settings** a
sign-in page ("Sign in with *your provider*") sits in front of everything: the Studio, every map,
and the hot-reload websocket. Only people in the allowed groups get in.
## Deploy
1. Build the image from [`Dockerfile`](./Dockerfile). It's a small bootstrap (Bun, git, and the
[oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) binary); at start it clones the repo
and runs [`entry.sh`](./entry.sh) from it, so the recipe updates with the repo. Paste it into any
container host (Coolify's "Dockerfile" build pack, Railway, `docker build`).
2. Set the environment below and expose the container's `PORT` over HTTPS.
3. Give it about 2 GB of memory (Vite plus the dependency install).
| Variable | Needed | Meaning |
|---|---|---|
| `REPO_URL` | yes | the dx-maps git URL to clone |
| `BRANCH` | no (`main`) | the branch to follow |
| `PULL_INTERVAL` | no (`15`) | seconds between pulls |
| `GIT_TOKEN` | private repos | a read token for the clone and pulls |
| `PORT` | no (`5173`) | the port the container serves on |
| `HMR_CLIENT_PORT`, `HMR_PROTOCOL` | behind HTTPS | `443` and `wss`, so browsers reach the hot-reload websocket |
## Sign-in (optional)
Set all of these to turn the gate on. Register a **confidential** client at your OIDC provider
with the redirect URL `https://<your-host>/oauth2/callback`; PKCE (S256) is used as well.
| Variable | Meaning |
|---|---|
| `OIDC_ISSUER` | the provider's issuer URL (its `/.well-known/openid-configuration` must resolve) |
| `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` | the registered client |
| `OIDC_REDIRECT_URL` | `https://<your-host>/oauth2/callback` |
| `OIDC_ALLOWED_GROUPS` | comma-separated groups allowed in; **required** (the server refuses to start without it, because many providers allow open sign-up) |
| `OIDC_DISPLAY_NAME` | the name on the button, e.g. "Sign in with *Acme*" |
| `OIDC_GROUPS_CLAIM` | the token claim holding groups (default `groups`) |
| `OIDC_COOKIE_SECRET` | 32 characters; keeps people signed in across restarts (random if unset) |
Secrets reach oauth2-proxy through its environment, never its command line.

65
deployment/cloud/entry.sh Executable file
View file

@ -0,0 +1,65 @@
#!/bin/bash
# dx-maps cloud dev server, run by the image's /boot.sh from the cloned repo (see Dockerfile).
# 1. install, 2. pull BRANCH every PULL_INTERVAL s (Vite hot-reloads what changed),
# 3. serve: Vite alone, or behind an OIDC sign-in when OIDC_ISSUER is set.
set -u
cd /app/repo
BRANCH="${BRANCH:-main}"; PORT="${PORT:-5173}"
echo "$(date -u +%FT%TZ) START at $(git rev-parse --short HEAD)" | tee -a /tmp/pull.log
bun install --frozen-lockfile || bun install
(
while true; do
sleep "${PULL_INTERVAL:-15}"
OLD=$(git rev-parse HEAD)
if git fetch -q --depth 1 origin "$BRANCH"; then
NEW=$(git rev-parse FETCH_HEAD)
if [ "$OLD" != "$NEW" ]; then
git reset -q --hard FETCH_HEAD
echo "$(date -u +%FT%TZ) PULLED ${OLD:0:8} -> ${NEW:0:8}" >> /tmp/pull.log
fi
fi
done
) &
CONFIG=/app/repo/deployment/cloud/vite.cloud.config.mjs
if [ -z "${OIDC_ISSUER:-}" ]; then
# Open, like a laptop: no sign-in.
export VITE_HOST=0.0.0.0 VITE_PORT="$PORT"
exec bun x vite --config "$CONFIG"
fi
# OIDC gate: oauth2-proxy owns the public port; Vite listens on localhost behind it, so the Plugin
# Studio, every map and the hot-reload websocket are all behind the same sign-in.
: "${OIDC_CLIENT_ID:?}" "${OIDC_CLIENT_SECRET:?}" "${OIDC_REDIRECT_URL:?}"
if [ -z "${OIDC_ALLOWED_GROUPS:-}" ]; then
echo "refusing to start: set OIDC_ALLOWED_GROUPS (comma-separated). With open sign-up, anyone could log in." >&2
exit 1
fi
ALLOW=(); IFS=',' read -ra G <<< "$OIDC_ALLOWED_GROUPS"
for g in "${G[@]}"; do g="${g// /}"; [ -n "$g" ] && ALLOW+=(--allowed-group="$g"); done
# Secrets reach oauth2-proxy through its environment, never its command line.
export OAUTH2_PROXY_CLIENT_SECRET="$OIDC_CLIENT_SECRET"
export OAUTH2_PROXY_COOKIE_SECRET="${OIDC_COOKIE_SECRET:-$(head -c 32 /dev/urandom | base64 | tr -d '=+/' | head -c 32)}"
VITE_HOST=127.0.0.1 VITE_PORT=5174 bun x vite --config "$CONFIG" &
oauth2-proxy \
--provider=oidc \
--oidc-issuer-url="$OIDC_ISSUER" \
--client-id="$OIDC_CLIENT_ID" \
--redirect-url="$OIDC_REDIRECT_URL" \
--code-challenge-method=S256 \
--scope="${OIDC_SCOPE:-openid profile email groups}" \
--oidc-groups-claim="${OIDC_GROUPS_CLAIM:-groups}" \
"${ALLOW[@]}" \
--email-domain='*' \
--provider-display-name="${OIDC_DISPLAY_NAME:-your identity provider}" \
--upstream=http://127.0.0.1:5174/ \
--http-address="0.0.0.0:$PORT" \
--reverse-proxy=true \
--cookie-secure=true \
--cookie-samesite=lax &
wait -n
exit $?

View file

@ -0,0 +1,22 @@
// Overlay for the cloud dev server: the repo's own vite.config.js plus what a shared, public
// host needs. Every value comes from the environment (see deployment/cloud/README.md).
import base from '/app/repo/vite.config.js'
import { mergeConfig } from 'vite'
const env = process.env
export default async (e) => {
const b = typeof base === 'function' ? await base(e) : base
const hmr = env.HMR_CLIENT_PORT
? { clientPort: Number(env.HMR_CLIENT_PORT), protocol: env.HMR_PROTOCOL || 'wss' }
: undefined
return mergeConfig(b, {
cacheDir: '/tmp/vite-cache',
server: {
host: env.VITE_HOST || '0.0.0.0',
port: Number(env.VITE_PORT || 5173),
strictPort: true,
allowedHosts: true,
...(hmr ? { hmr } : {}),
},
})
}

View file

@ -0,0 +1,61 @@
/**
* internal/backend-dev-plugin.mjs — `bun run dev` starts the local plugin backends.
*
* WHY: some plugins ship to DESKTOP with a local backend (rudder-voice → backend/bin/rudder-engine,
* run as a Tauri sidecar). In the Plugin Studio the same backend must be running, or the plugin can't
* be tested in the browser. This plugin starts each built backend with the dev server, forwards its
* /api/<name>/ routes, and stops it when the dev server stops. A backend that isn't built is skipped
* with a hint: plugins without a backend never notice this file.
*
* DEV ONLY (`apply: 'serve'`). A production build never starts or bundles a backend.
*/
import { spawn } from 'node:child_process'
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
// name → how to run it. `route` is proxied to `port` on 127.0.0.1.
export const DEV_BACKENDS = [
{
name: 'rudder-engine',
bin: 'backend/bin/rudder-engine',
build: 'cd backend/rudder-engine && go build -o ../bin/rudder-engine .',
route: '/api/rudder',
port: Number(process.env.RUDDER_PORT || 5193),
env: { RUDDER_PORT: String(process.env.RUDDER_PORT || 5193), RUDDER_NEMO_PORT: String(process.env.RUDDER_NEMO_PORT || 5194) },
},
]
// The proxy table for vite's `server.proxy`. SSE (`/events`) streams through unbuffered.
export function devBackendProxy() {
return Object.fromEntries(DEV_BACKENDS.map((b) => [b.route, { target: `http://127.0.0.1:${b.port}`, changeOrigin: false }]))
}
export function backendDevPlugin(root) {
const children = []
return {
name: 'dx-backend-dev',
apply: 'serve',
configureServer(server) {
for (const b of DEV_BACKENDS) {
const bin = resolve(root, b.bin)
if (!existsSync(bin)) {
server.config.logger.warn(`[backend] ${b.name} not built — its plugin will show "engine offline". Build: ${b.build}`)
continue
}
const child = spawn(bin, [], { cwd: root, env: { ...process.env, ...b.env }, stdio: ['ignore', 'pipe', 'pipe'] })
const tag = `[${b.name}]`
const out = (d) => String(d).trim().split('\n').forEach((l) => l && server.config.logger.info(`${tag} ${l}`))
child.stdout.on('data', out)
child.stderr.on('data', out)
child.on('exit', (code) => server.config.logger.warn(`${tag} exited (${code})`))
children.push(child)
server.config.logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`)
}
const stop = () => children.forEach((c) => { try { c.kill('SIGTERM') } catch {} })
server.httpServer?.on('close', stop)
process.once('exit', stop)
process.once('SIGINT', () => { stop(); process.exit(0) })
process.once('SIGTERM', () => { stop(); process.exit(0) })
},
}
}

View file

@ -123,7 +123,7 @@ function hostPage(kind, s, eng, token) {
body = snippet(s.bundle, s.profile, 'script')
.replaceAll('src="./', `src="${BASE}/`).replaceAll('href="./', `href="${BASE}/`)
.replaceAll('YOUR_MAPBOX_TOKEN', token).replaceAll('YOUR_SOURCE', 'shelf').replaceAll('YOUR_TREE_URL', BASE)
if (eng === 'maplibre') body = body.replace(/<!--[^>]*remove this line[^>]*-->\s*<link[^>]*>\s*/i, '')
if (eng === 'maplibre') body = body.replace(/<!--[^>]*(?:remove this line|this line must go)[^>]*-->\s*<link[^>]*>\s*/i, '')
.replace(/profile-config='\{/, `profile-config='{"provider":"maplibre",`)
}
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>PARTNER ${kind}</title>

View file

@ -241,6 +241,13 @@ const r = await page.evaluate(() => {
// presence is DETECTED rather than assumed — see the sheet arithmetic below, which must stay
// exact about list pages whether or not this page is there.
funnel: document.querySelectorAll('.funnel-title').length,
lifeSheets: document.querySelectorAll('.sheet-life').length,
christianSheets: document.querySelectorAll('.sheet-christian').length,
lifeCells: document.querySelectorAll('.lc-grid .lc-c').length,
prayBoxes: document.querySelectorAll('.pray-box').length,
funnelTip: document.querySelectorAll('.funnel-tip').length,
lifeShaded: document.querySelectorAll('.lc-grid .lc-reached').length,
funnelEq: document.querySelectorAll('.funnel-eq').length,
funnelSteps: document.querySelectorAll('.funnel-mine').length,
// ★ MEASURED SO THE GATE CAN REPORT WHAT IT ASSERTED. rega: "the gate prints no funnel line, even
// with --expect-funnel ... a reader cannot see WHAT it asserted about page 2." A silent pass is
@ -645,12 +652,44 @@ if (!boxes.length) {
// this assertion exists to catch SILENTLY DROPPED PEOPLE GROUPS, and a ±1 tolerance is exactly the
// slack a lost list page would hide in. So the funnel is DETECTED (.funnel-title) and added as a
// known term, leaving the list-page arithmetic as strict as it was.
// ⚠ AND THE SAME FOR THE LIFE-CALENDAR PAGES (2026-09-27, Driver's page order). Two more structural
// sheets — a 100-year life in weeks, then the SAME grid shaded by the evangelical share of these
// groups' population — so they are DETECTED and added as known terms for exactly the reason the
// funnel was, and for the reason stated above: a tolerance here would hide a dropped list page.
const PER_PAGE = 20
const funnelSheets = r.funnel ? 1 : 0
const expectedSheets = 1 + funnelSheets + Math.ceil(r.cards / PER_PAGE) + 2
const lifeSheets = (r.lifeSheets || 0) + (r.christianSheets || 0)
const expectedSheets = 1 + lifeSheets + funnelSheets + Math.ceil(r.cards / PER_PAGE) + 2
// ── THE LIFE-CALENDAR PAGES, asserted rather than merely counted ──────────────────────────────────
// A page that EXISTS is not a page that is RIGHT: an empty grid, a grid of the wrong size, or a
// christian page whose shading silently fell to zero would all still be one sheet. 5,200 is the
// Driver's own arithmetic (100 years x 52 weeks) and is the only number here that may not drift.
if (r.lifeSheets) {
const EXPECT = 5200
const perGrid = r.lifeCells / (r.lifeSheets + (r.christianSheets || 0))
if (perGrid !== EXPECT) {
fails.push(`the life calendar should be ${EXPECT} cells per grid (100 years x 52 weeks) `
+ `but rendered ${perGrid} — the grid changed size, so the page no longer shows a human life`)
}
}
// ★ THE SHADED SHARE MUST BE NON-ZERO AND NOT EVERYTHING. 0 shaded means the percentage never
// reached the page (a null christian-share file renders the page with an empty grid, which reads as
// "nobody is reached" — a false claim rather than missing data); 5,200 means the maths inverted.
if (r.christianSheets) {
const shaded = r.lifeShaded ?? null
if (shaded !== null && (shaded <= 0 || shaded >= 5200)) {
fails.push(`the christian-share page shaded ${shaded} of 5200 cells — 0 would claim nobody is `
+ `reached and 5200 would claim everybody is; both mean the percentage did not arrive`)
}
}
// The three prayer columns: one tick box per group per question, so the count is exactly 3 x cards.
if (r.cards && r.prayBoxes !== r.cards * 3) {
fails.push(`${r.cards} people cards should carry ${r.cards * 3} prayer tick boxes `
+ `(blessed / what to know / what to do) but the packet has ${r.prayBoxes}`)
}
if (r.cards && r.sheets !== expectedSheets) {
fails.push(`${r.cards} people cards should lay out as ${expectedSheets} sheets `
+ `(1 map${funnelSheets ? ' + 1 funnel' : ''} + ${Math.ceil(r.cards / PER_PAGE)} list + points + wheel), `
+ `(1 map${lifeSheets ? ` + ${lifeSheets} life-calendar` : ''}${funnelSheets ? ' + 1 funnel' : ''} + ${Math.ceil(r.cards / PER_PAGE)} list + points + wheel), `
+ `but the packet has ${r.sheets} — either groups were dropped from a page or the `
+ `${PER_PAGE}-per-page grid changed`)
}
@ -690,7 +729,11 @@ if (r.funnel && r.funnelRows !== r.funnelBars) {
// description beside it ("frontier people groups") is prose and must be translated.
// If a third entry is ever needed, say in the entry whether it is settled or deferred — an allowlist
// that does not distinguish the two turns an open defect into a verdict.
const LATIN_OK = new Set(['IMB', 'peoplegroups.org', 'Project'])
// 'UPG-' is the Latin run left by "UPG-100", the PRODUCT NAME, which every locale keeps verbatim —
// the same reason 'Project' is here (Project 4700 / 700 are list names, not prose). Verified it is the
// only Latin left in the ar funnel page after the tip and sidebar strings were genuinely translated:
// an allowlist entry added BEFORE translating would have hidden fifteen untranslated strings.
const LATIN_OK = new Set(['IMB', 'peoplegroups.org', 'Project', 'UPG-'])
const NON_LATIN_LOCALES = new Set(['hi', 'ar', 'ru', 'zh', 'he', 'fa', 'ur', 'bn', 'ta', 'th', 'ko', 'ja'])
if (r.funnel && NON_LATIN_LOCALES.has(String(lang).split('-')[0])) {
const stray = (r.funnelLatinRuns || []).filter((x) => !LATIN_OK.has(x))

View file

@ -49,6 +49,12 @@ if (!en) { console.error('PACKET-STRINGS-PARITY: FAIL — no English block to co
const DECLARED_ABSENT = {
point4_passage: 'Mark 5:1-20 in full — public-domain licensing per language (DECIDE-MARK5-…)',
point4_passage_credit: 'the attribution line for the passage above',
// ── The Driver's new packet pages, 2026-09-27: a 100-year life calendar, the same grid shaded by
// the evangelical share of these groups' population, and the funnel's equation sidebar + tip.
// ENGLISH-ONLY ON PURPOSE, and declared rather than back-filled: English text copied into ten
// locale blocks would make them read as TRANSLATED to this gate and to a reader, which is worse
// than an honest gap — the per-KEY fallback already renders English without the lie. These come out
// of this list one at a time as real translations arrive.
}
// Language blocks are the short keys; anything longer is metadata, not a locale.

View file

@ -106,6 +106,15 @@ const RULES = [
&& /(?:toLocaleString|NumberFormat|DateTimeFormat)\s*\(\s*`[^`]*\$\{/.test(before)
if (!isFallback) out.push(m.index)
}
// ★ THE NAMED CONSTANT COUNTS TOO, or folding the literal into one module LOOKS like fixing it.
// library/utils/formatCount.js now holds the fleet's en-US decision and calls
// toLocaleString(COUNT_LOCALE) — no literal — so the pattern above went quiet and 8 of the
// tree's 13 warns vanished with the BEHAVIOUR unchanged. That is the same false-green as a
// precondition that skips the file under test: the audit stopped seeing a decision still in
// force. One site, attributed to every bundle that imports it, is what "held in one place"
// should read as in the matrix — not zero.
const constRe = /COUNT_LOCALE\s*=\s*['"]en(?:-US)?['"]/g
while ((m = constRe.exec(src))) out.push(m.index)
return out
} },
// A whole-file rule: the fetch and the helper need not be on the same line, only in the same file.

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
"upg100WallTag": "تعبئة للصلاة",
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>.",
"upg100WallIntroA": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم ",
"upg100WallIntroEmph": "باسم شعبهم",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "Meine UPG-100-Liste — Gebetslisten und Poster",
"upg100WallTag": "Gebetsmobilisierung",
"upg100WallIntro": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie <strong>mit dem Namen ihrer Volksgruppe</strong> beten.",
"upg100WallIntroA": "Teilen Sie die Gebetsaufgabe in Gruppen zu fünfzig und hundert auf — so wie Jesus die 5.000 speiste (Lukas 9,14). Jedes Poster ist eine 100er-Liste, die Sie beten, drucken und an die Wand hängen können — und für sie ",
"upg100WallIntroEmph": "mit dem Namen ihrer Volksgruppe",
"upg100WallIntroB": " beten.",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "My UPG 100 List — Prayer Lists & Posters",
"upg100WallTag": "Prayer Mobilization",
"upg100WallIntro": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them <strong>by their people group name</strong>.",
"upg100WallIntroA": "Hand away the prayer task in clusters of fifties and hundreds — the way Jesus fed the 5,000 (Luke 9:14). Each poster is a 100-list you can pray, print, and put on a wall — praying for them ",
"upg100WallIntroEmph": "by their people group name",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "Mi lista UPG 100 — Listas de oración y carteles",
"upg100WallTag": "Movilización de oración",
"upg100WallIntro": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos <strong>por el nombre de su pueblo</strong>.",
"upg100WallIntroA": "Reparte la tarea de la oración en grupos de cincuenta y de cien, como Jesús alimentó a los 5.000 (Lucas 9:14). Cada cartel es una lista de 100 que puedes orar, imprimir y poner en la pared — orando por ellos ",
"upg100WallIntroEmph": "por el nombre de su pueblo",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "Ma liste UPG 100 — Listes de prière et affiches",
"upg100WallTag": "Mobilisation pour la prière",
"upg100WallIntro": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux <strong>par le nom de leur peuple</strong>.",
"upg100WallIntroA": "Répartissez la tâche de la prière par groupes de cinquante et de cent — comme Jésus a nourri les 5 000 (Luc 9:14). Chaque affiche est une liste de 100 que vous pouvez prier, imprimer et afficher au mur — en priant pour eux ",
"upg100WallIntroEmph": "par le nom de leur peuple",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "मेरी UPG 100 सूची — प्रार्थना सूचियाँ और पोस्टर",
"upg100WallTag": "प्रार्थना लामबंदी",
"upg100WallIntro": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए <strong>उनके जनसमूह के नाम से</strong> प्रार्थना करते हुए।",
"upg100WallIntroA": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए ",
"upg100WallIntroEmph": "उनके जनसमूह के नाम से",
"upg100WallIntroB": " प्रार्थना करते हुए।",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "La mia lista UPG 100 — Liste di preghiera e poster",
"upg100WallTag": "Mobilitazione alla preghiera",
"upg100WallIntro": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro <strong>con il nome del loro popolo</strong>.",
"upg100WallIntroA": "Distribuisci il compito della preghiera in gruppi di cinquanta e di cento — come Gesù sfamò i 5.000 (Luca 9:14). Ogni poster è una lista di 100 che puoi pregare, stampare e appendere al muro — pregando per loro ",
"upg100WallIntroEmph": "con il nome del loro popolo",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "A minha lista UPG 100 — Listas de oração e cartazes",
"upg100WallTag": "Mobilização de oração",
"upg100WallIntro": "Reparta a tarefa da oração em grupos de cinquenta e de cem — como Jesus alimentou os 5.000 (Lucas 9:14). Cada cartaz é uma lista de 100 que pode orar, imprimir e pôr na parede — orando por eles <strong>pelo nome do seu povo</strong>.",
"upg100WallIntroA": "Reparta a tarefa da oração em grupos de cinquenta e de cem — como Jesus alimentou os 5.000 (Lucas 9:14). Cada cartaz é uma lista de 100 que pode orar, imprimir e pôr na parede — orando por eles ",
"upg100WallIntroEmph": "pelo nome do seu povo",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "Lista mea UPG 100 — Liste de rugăciune și afișe",
"upg100WallTag": "Mobilizare pentru rugăciune",
"upg100WallIntro": "Împarte sarcina rugăciunii în grupuri de câte cincizeci și o sută — așa cum Isus a hrănit cei 5.000 (Luca 9:14). Fiecare afiș este o listă de 100 pe care o poți ruga, tipări și pune pe perete — rugându-te pentru ei <strong>pe numele poporului lor</strong>.",
"upg100WallIntroA": "Împarte sarcina rugăciunii în grupuri de câte cincizeci și o sută — așa cum Isus a hrănit cei 5.000 (Luca 9:14). Fiecare afiș este o listă de 100 pe care o poți ruga, tipări și pune pe perete — rugându-te pentru ei ",
"upg100WallIntroEmph": "pe numele poporului lor",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "Мой список UPG 100 — молитвенные списки и плакаты",
"upg100WallTag": "Молитвенная мобилизация",
"upg100WallIntro": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них <strong>по имени их народа</strong>.",
"upg100WallIntroA": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них ",
"upg100WallIntroEmph": "по имени их народа",
"upg100WallIntroB": ".",

View file

@ -5,7 +5,6 @@
},
"upg100WallHeading": "我的 UPG 100 名單 — 禱告名單與海報",
"upg100WallTag": "禱告動員",
"upg100WallIntro": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — <strong>按著他們族群的名字</strong>為他們禱告。",
"upg100WallIntroA": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — ",
"upg100WallIntroEmph": "按著他們族群的名字",
"upg100WallIntroB": "為他們禱告。",

View file

@ -74,6 +74,14 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
const SCALE = (PAGE_W_IN * 96 / W).toFixed(6); // print scale: one sheet = one page
const M = 60, HEAD = 90; // margins + running header
const COLS = 2, ROWS = 10, PER_PAGE = COLS * ROWS;
// ★ THE THREE PRAYER COLUMNS, the Driver's own three questions (2026-09-27), in his order:
// 1 have I blessed them
// 2 God, what do you want me to KNOW about them
// 3 God, what do you want me to DO to help them
// A tick box per group per question, printed on the card so the packet is a WORKING sheet rather
// than a reference: the reader marks it as they pray. Deliberately three and not more — the brief
// is one hour with God, and a fourth column turns a prayer aid into a form.
const PRAY_COLS = ['pray_blessed', 'pray_know', 'pray_do'];
const GUT = 14;
const CW = (W - 2 * M - GUT * (COLS - 1)) / COLS;
const CH = (H - 2 * M - HEAD - GUT * (ROWS - 1)) / ROWS;
@ -117,6 +125,41 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
page-break-after:always;break-after:page}
.sheet:last-child{page-break-after:auto;break-after:auto}
.hd{position:absolute;left:${M}px;top:46px;font:700 30px Georgia,'Palatino Linotype',serif;color:#1b1a17}
/* ── LIFE CALENDAR (pages 2 and 3) ─────────────────────────────────────
5,200 cells must fit one sheet and still read as countable boxes. 52 columns
is fixed by the DATA (weeks in a year), so the cell size is derived from the
printable width rather than chosen: (1500 - 2*60) / 52 = 26.5px per column,
leaving a 21px box with a 2px gap. Measured, not eyeballed — a hardcoded box
size that happened to fit at one margin would break the moment M changed. */
.pg-head{margin:0 0 10px}
.pg-kicker{font:700 15px Georgia,serif;letter-spacing:.14em;text-transform:uppercase;color:#8a8577}
.pg-h1{margin:2px 0 0;font:700 40px Georgia,serif;color:#1b1a17}
.pg-lede{margin:0 0 22px;max-width:1180px;font:400 19px/1.5 Georgia,serif;color:#413d35}
/* ⚠ THE CELLS ARE RECTANGLES, NOT SQUARES, and that is forced by the sheet rather than chosen.
52 columns is fixed by the data (weeks in a year) and 100 rows by the Driver's "100 year life",
so both dimensions are given and only the SHAPE is free. Squares would need
(1380/52)=26.5px per side x 100 rows = 2,654px of grid in 1,521px of printable height — it
overflows the page by 1.7x. Caught by computing the height before rendering rather than by
looking at an overflowing PDF. 13px rows put 100 rows in 1,300px with room for the legend. */
.lc-grid{display:grid;grid-template-columns:repeat(52,1fr);grid-auto-rows:13px;gap:2px;width:100%}
.lc-c{display:block;width:100%;height:100%;border:1px solid #d8d3c4;border-radius:1px;background:#fff}
.lc-on{background:#b9b2a0;border-color:#b9b2a0}
/* The shaded share on page 3 is the REACHED share, so it takes the accent colour and the
unshaded majority stays quiet — the visual weight belongs to what is still waiting. */
.lc-reached{background:#2f6d4f;border-color:#2f6d4f}
.lc-legend{display:flex;gap:34px;margin:20px 0 0;font:400 17px Georgia,serif;color:#413d35}
.lc-legend i{display:inline-block;width:15px;height:15px;vertical-align:-2px;margin-inline-end:8px}
.lc-foot{margin:14px 0 0;font:400 14px Georgia,serif;color:#6f6a5c}
/* ── THE THREE PRAYER COLUMNS on every group card ───────────────────────
Right-aligned inside the 683x160 card, which leaves ~412px for the name and
numbers (measured from CW/CH rather than assumed). inset-inline-end, not right,
so the column stack mirrors under dir=rtl with the rest of the card. */
.pray-cols{position:absolute;inset-inline-end:10px;top:12px;display:flex;flex-direction:column;gap:9px}
.pray-col{display:flex;align-items:center;gap:7px;font:400 11px Georgia,serif;color:#6f6a5c;white-space:nowrap}
.pray-box{display:inline-block;width:17px;height:17px;border:1.5px solid #b0a993;border-radius:3px;background:#fff;flex:0 0 auto}
.pray-col em{font-style:normal}
.hd-r{position:absolute;right:${M}px;top:54px;font:400 19px Georgia,serif;color:#8c8170}
.card{position:absolute;display:flex;gap:12px;align-items:center;
border-bottom:1px solid #e2ddd0;padding-bottom:6px}
@ -411,24 +454,131 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
// FOOTNOTE rather than as the conclusion of the "43" row. The gap belongs BETWEEN rows; the share
// is that last row's conclusion and should sit close under it. Proximity is the only thing
// grouping them, so an even gap everywhere groups the share with the wrong element.
return `<div style="margin-bottom:${r.mine ? 14 : GAP}px"${r.mine ? ' class="funnel-mine-row"' : ''}>
// ★ CENTRED, so the bars form an UPSIDE-DOWN TRIANGLE narrowing to a point — the Driver's
// "upside down triangle ... the bottom of the triangle shows the tip". margin-inline:auto on
// the bar and text-align:center on the row is the whole geometry: each bar is already drawn
// proportional to the first step, so centring them IS the triangle. No new maths, and the
// careful population/grouping logic below is untouched.
return `<div style="margin-bottom:${r.mine ? 14 : GAP}px;text-align:center"${r.mine ? ' class="funnel-mine-row"' : ''}>
<div${r.mine ? ' class="funnel-mine"' : ''} style="font:700 ${r.mine ? 46 : 40}px Georgia,serif;color:${r.tint ? '#7a3b2e' : '#1b1a17'};line-height:1">${num(r.groups)}</div>
<div style="font:400 ${r.mine ? 22 : 20}px Georgia,serif;color:#3b3226;margin-top:3px">${esc(r.label)}</div>
${pop}
<div style="height:16px;width:${width(r.groups)}px;background:${r.tint ? '#7a3b2e' : '#cdbf9e'};margin-top:8px;border-radius:2px"></div>
<div style="height:16px;width:${width(r.groups)}px;background:${r.tint ? '#7a3b2e' : '#cdbf9e'};margin-top:8px;border-radius:2px;margin-inline:auto"></div>
</div>`;
}).join('');
// ── THE EQUATION SIDEBAR ────────────────────────────────────────────────
// Driver, 2026-09-27: "a map equation with a side bar for the equations". Every number on this
// page is DERIVED, and the derivation is the argument — a reader who cannot see how 12,379
// becomes 2,107 has to take the funnel on faith. So each step states the filter that produced it,
// read from vision-funnel.json's own step ids rather than written out here, and the provenance
// line names the file and capture date.
// ⚠ NOT A RESTATEMENT OF THE BARS. The bars show HOW MANY; the sidebar shows WHY THAT MANY. A
// sidebar repeating the counts would add width and no information.
const SIDEBAR_W = 300;
const eqRows = allRows.filter((r) => !r.mine).map((r, idx, arr) => {
const prev = idx > 0 ? arr[idx - 1] : null;
const delta = prev ? Number(prev.groups) - Number(r.groups) : null;
return `<div style="margin-bottom:16px">
<div style="font:700 16px Georgia,serif;color:#3b3226">${num(r.groups)}</div>
<div style="font:400 12px/1.45 Georgia,serif;color:#6f6a5c">${esc(r.equation || r.label)}</div>
${delta !== null ? `<div style="font:400 11px Georgia,serif;color:#a89f8c;margin-top:2px">\u2212 ${num(delta)}</div>` : ''}
</div>`;
}).join('');
const sidebar = `<div class="funnel-eq" style="position:absolute;top:${ROWS_TOP}px;inset-inline-end:${M}px;width:${SIDEBAR_W}px">
<div style="font:700 13px Georgia,serif;letter-spacing:.1em;text-transform:uppercase;color:#8a8577;margin-bottom:12px">${esc(t('funnel_eq_title'))}</div>
${eqRows}</div>`;
// ★ THE TIP OF THE TRIANGLE: this packet is one of N. The Driver's "the tip ... the smaller UPG
// 100 list is a subset, 1 of 30 or n exact UPG 100 lists". N is READ from the catalogue
// (funnel.listCount), never typed — it is 28 today and was 27 before the deaf zone became
// reachable, so a literal would already be wrong.
const tip = !funnel.listCount ? '' : `<div class="funnel-tip" style="text-align:center;margin-top:18px">
<div style="font:700 15px Georgia,serif;letter-spacing:.08em;text-transform:uppercase;color:#7a3b2e">${esc(fill(t('funnel_tip'), { n: num(funnel.listCount) }))}</div>
</div>`;
return sheet(`
<div style="position:absolute;left:${M}px;top:44px;right:${M}px">
<div class="funnel-title" style="font:700 40px Georgia,serif;color:#1b1a17">${esc(t('funnel_title'))}</div>
<div style="font:italic 18px Georgia,serif;color:#6d766e;margin-top:6px">${esc(t('funnel_intro'))}</div>
</div>
<div style="position:absolute;left:${M}px;top:${ROWS_TOP}px;right:${M}px">${rows}${share}</div>
<div style="position:absolute;left:${M}px;top:${ROWS_TOP}px;right:${SIDEBAR_W + M + 24}px">${rows}${share}${tip}</div>
${sidebar}
<div class="fine">${foot}</div>`);
}
// ── PAGES 3..n: the people groups, 20 per sheet ─────────────────────────────
// ── LIFE CALENDAR: 100 years x 52 weeks = 5200 boxes ────────────────────────
//
// Driver, 2026-09-27: page 2 is "a life calendar of the years of a human life in weeks", and page 3
// is "the same life cal used for percentage of Christian on average for dx people groups". ONE grid,
// TWO readings — which is the whole point. A percentage drawn as a bar is an abstraction a reader
// has to translate; drawn on the grid they have just understood as their OWN LIFE, it is the same
// 5200 boxes they were looking at a page earlier.
//
// ★ 5,200 IS NOT ROUNDED OR CHOSEN FOR LAYOUT: 100 years x 52 weeks, the Driver's own numbers
// ("a 100 year life in weeks"). Everything else here is derived from it, so the two pages cannot
// drift apart into two grids that merely look alike.
function lifeGrid({ shaded = 0, shadeClass = 'lc-on' } = {}) {
const YEARS = 100, WEEKS = 52, TOTAL = YEARS * WEEKS;
const n = Math.max(0, Math.min(TOTAL, Math.round(Number(shaded) || 0)));
// Row-major, so the shaded run reads as a contiguous block from the top — a scattered
// shading of the same COUNT would be honest arithmetic and an unreadable picture.
const cells = [];
for (let i = 0; i < TOTAL; i++) {
cells.push(`<i class="lc-c${i < n ? ' ' + shadeClass : ''}"></i>`);
}
return { TOTAL, YEARS, WEEKS, shadedCount: n, html: `<div class="lc-grid">${cells.join('')}</div>` };
}
// Page 2 — a human life, unshaded except for the weeks already lived if the caller supplies an age.
// No age is supplied today: the packet is printed for a reader we do not know, so shading their life
// would be a guess presented as a fact. The empty grid is the honest version and still lands the
// point — 5,200 boxes is a whole life on one page.
function lifePage(zone) {
const g = lifeGrid();
return sheet(`
<div class="pg-head"><div class="pg-kicker">${esc(t('life_kicker'))}</div>
<h1 class="pg-h1">${esc(t('life_title'))}</h1></div>
<p class="pg-lede">${esc(t('life_lede'))}</p>
${g.html}
<p class="lc-foot">${esc(t('life_foot'))}</p>
`, 'sheet-life');
}
// Page 3 — the SAME grid, shaded by the share of the DX population that is evangelical.
//
// ⚠ THE NUMBER IS POPULATION-WEIGHTED, NOT A GROUP COUNT, because the Driver asked for
// "% of Christian population vs lost". Those differ sharply: 69.1% of the 2,091 matched DX GROUPS
// have no known evangelicals, but weighted by POPULATION the evangelical share is 0.61% of
// 209,079,159 people — 32 boxes of 5,200. A group-count version would have shown a different,
// milder picture of the same data.
// ⚠ AND THE BAND MIDPOINT IS AN ASSUMPTION, NAMED ON THE PAGE. peoplegroups.org publishes
// EvangelicalLevel as a BAND ("Less than 2%", "No Known Evangelicals", …), never a number, so a
// percentage requires choosing a value inside each band. The builder uses the band's midpoint and
// 0 for "No Known Evangelicals"; the footnote says so, because a computed figure whose method is
// invisible reads as a measurement.
function christianPage(zone, christian) {
if (!christian || !Number.isFinite(Number(christian.pctEvangelical))) return '';
const pct = Number(christian.pctEvangelical);
const g = lifeGrid({ shaded: (pct / 100) * 5200, shadeClass: 'lc-reached' });
const lostPct = 100 - pct;
// ⚠ TWO DECIMALS ON BOTH SIDES, not rounded above 10. The reached share is 0.61% and rounding the
// other side to "99%" loses the asymmetry that is the whole point: 99.39 and 0.61 sum to 100, while
// 99 and 0.61 do not, and a reader checking the arithmetic finds the page wrong.
const fmtPct = (x) => x.toFixed(2).replace(/\.?0+$/, '');
return sheet(`
<div class="pg-head"><div class="pg-kicker">${esc(t('christian_kicker'))}</div>
<h1 class="pg-h1">${esc(t('christian_title'))}</h1></div>
<p class="pg-lede">${esc(t('christian_lede'))}</p>
${g.html}
<div class="lc-legend">
<span><i class="lc-c lc-reached"></i> ${esc(t('christian_reached'))} — ${esc(fmtPct(pct))}% · ${num(g.shadedCount)} ${esc(t('life_weeks'))}</span>
<span><i class="lc-c"></i> ${esc(t('christian_lost'))} — ${esc(fmtPct(lostPct))}% · ${num(g.TOTAL - g.shadedCount)} ${esc(t('life_weeks'))}</span>
</div>
<p class="lc-foot">${esc(t('christian_foot'))}${christian.population ? ` · ${num(christian.population)} ${esc(t('christian_people'))}` : ''}</p>
`, 'sheet-christian');
}
function listPages(zone, groups, photos) {
const pages = [];
for (let start = 0; start < groups.length; start += PER_PAGE) {
@ -447,7 +597,10 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
<div class="nm">${i + 1}. ${esc(g.name)}</div>
<div class="mt" style="margin-top:4px">${esc(flat(g.religion))}</div>
<div class="mt">${num(g.population)} ${esc(t('people_suffix'))} · ${esc(flat(g.country_code))}</div>
</div></div>`;
</div>
<div class="pray-cols">${PRAY_COLS.map((k) => `<span class="pray-col"
><i class="pray-box"></i><em>${esc(t(k))}</em></span>`).join('')}</div>
</div>`;
}).join('');
pages.push(sheet(`
<div class="hd">${esc(zone.label || zone.id)}</div>
@ -551,9 +704,20 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
// working unchanged and a host with no funnel data still gets a complete, correct packet. A page
// that cannot state true numbers must not appear at all — better a 4-section packet than one
// asserting figures it could not read.
function buildPacket(zone, groups, photos, mapSvg, funnel = null) {
function buildPacket(zone, groups, photos, mapSvg, funnel = null, christian = null) {
// ★ PAGE ORDER IS THE DRIVER'S, 2026-09-27, and it is an argument rather than a layout:
// 1 map — where this zone is
// 2 life calendar — 100 years x 52 weeks, a whole human life on one page
// 3 the SAME grid — shaded by the evangelical share of these groups' POPULATION
// 4 funnel — 12,379 -> 7,341 -> 4,754 -> 2,107 -> this list, 1 of N
// 5..n people groups with photos and the three prayer columns
// then the 12 prayer points and the prayer wheel
// Pages 2 and 3 are deliberately adjacent: the reader learns to read 5,200 boxes as their own
// life, and then sees the same 5,200 boxes answer "how many of these people know Christ".
const pages = [
mapPage(zone, groups, mapSvg),
lifePage(zone),
christianPage(zone, christian),
funnelPage(zone, groups, funnel),
...listPages(zone, groups, photos),
pointsPage(),
@ -569,5 +733,5 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
<body>${pages.join('\n')}</body></html>`;
}
return { buildPacket, mapPage, funnelPage, listPages, pointsPage, wheelPage, css, mapCss, PER_PAGE, W, H };
return { buildPacket, mapPage, lifePage, christianPage, funnelPage, listPages, pointsPage, wheelPage, css, mapCss, PER_PAGE, W, H };
}

View file

@ -184,7 +184,24 @@
"funnel_list_frontier-500k": "Project 700: frontier people groups of 500,000 or more",
"funnel_list_unengaged": "Unengaged unreached people groups",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} more"
"zone_join_more": "{a} + {n} more",
"life_kicker": "Perspective",
"life_title": "A human life, in weeks",
"life_lede": "One hundred years. Fifty-two weeks each. Five thousand two hundred boxes — and that is the whole of it. Every week you will ever pray is one of these.",
"life_foot": "100 years x 52 weeks = 5,200 weeks.",
"life_weeks": "weeks",
"christian_kicker": "The same 5,200 boxes",
"christian_title": "Who among them knows Christ",
"christian_lede": "This is the same grid — one human life in weeks. Now each box is a share of the people in this list. The shaded boxes are the evangelical share of their population; the rest are waiting.",
"christian_reached": "Evangelical",
"christian_lost": "Still waiting",
"christian_people": "people",
"christian_foot": "Population-weighted from peoplegroups.org EvangelicalLevel, taking each published band at its midpoint and 0 for \"No Known Evangelicals\"",
"pray_blessed": "Blessed them",
"pray_know": "What to know",
"pray_do": "What to do",
"funnel_eq_title": "How each number is reached",
"funnel_tip": "This packet is 1 of {n} UPG-100 lists"
},
"es": {
"map_page_kicker": "GRUPOS ÉTNICOS NO EVANGELIZADOS",
@ -361,7 +378,24 @@
"funnel_list_frontier-500k": "Proyecto 700: grupos étnicos de frontera de 500.000 o más",
"funnel_list_unengaged": "Grupos étnicos no alcanzados y sin obreros",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} más"
"zone_join_more": "{a} + {n} más",
"funnel_eq_title": "Cómo se obtiene cada número",
"funnel_tip": "Este cuadernillo es 1 de {n} listas UPG-100",
"life_kicker": "Perspectiva",
"life_title": "Una vida humana, en semanas",
"life_lede": "Cien años. Cincuenta y dos semanas cada uno. Cinco mil doscientas casillas — y eso es todo. Cada semana en que ores será una de estas.",
"life_foot": "100 años x 52 semanas = 5.200 semanas.",
"life_weeks": "semanas",
"christian_kicker": "Las mismas 5.200 casillas",
"christian_title": "Quiénes entre ellos conocen a Cristo",
"christian_lede": "Es la misma cuadrícula — una vida humana en semanas. Ahora cada casilla es una parte de las personas de esta lista. Las casillas sombreadas son la proporción evangélica de su población; el resto espera.",
"christian_reached": "Evangélicos",
"christian_lost": "Aún esperando",
"christian_people": "personas",
"christian_foot": "Ponderado por población a partir del EvangelicalLevel de peoplegroups.org, tomando cada banda publicada en su punto medio y 0 para «No Known Evangelicals»",
"pray_blessed": "Los bendije",
"pray_know": "Qué saber",
"pray_do": "Qué hacer"
},
"fr": {
"map_page_kicker": "GROUPES ETHNIQUES NON ATTEINTS",
@ -538,7 +572,24 @@
"funnel_list_frontier-500k": "Projet 700 : peuples frontières de 500 000 personnes ou plus",
"funnel_list_unengaged": "Peuples non atteints et sans ouvrier",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} autres"
"zone_join_more": "{a} + {n} autres",
"funnel_eq_title": "Comment chaque nombre est obtenu",
"funnel_tip": "Ce livret est 1 sur {n} listes UPG-100",
"life_kicker": "Mise en perspective",
"life_title": "Une vie humaine, en semaines",
"life_lede": "Cent ans. Cinquante-deux semaines chacune. Cinq mille deux cents cases — et c’est tout. Chaque semaine où vous prierez sera l’une d’elles.",
"life_foot": "100 ans x 52 semaines = 5 200 semaines.",
"life_weeks": "semaines",
"christian_kicker": "Les mêmes 5 200 cases",
"christian_title": "Qui parmi eux connaît le Christ",
"christian_lede": "C’est la même grille — une vie humaine en semaines. Chaque case représente maintenant une part des personnes de cette liste. Les cases colorées sont la part évangélique de leur population ; le reste attend.",
"christian_reached": "Évangéliques",
"christian_lost": "Toujours en attente",
"christian_people": "personnes",
"christian_foot": "Pondéré par la population à partir du EvangelicalLevel de peoplegroups.org, chaque tranche publiée étant prise à son point médian et 0 pour « No Known Evangelicals »",
"pray_blessed": "Je les ai bénis",
"pray_know": "Ce qu’il faut savoir",
"pray_do": "Ce qu’il faut faire"
},
"pt": {
"map_page_kicker": "GRUPOS ÉTNICOS NÃO ALCANÇADOS",
@ -715,7 +766,24 @@
"funnel_list_frontier-500k": "Projeto 700: grupos étnicos de fronteira de 500.000 ou mais",
"funnel_list_unengaged": "Grupos étnicos não alcançados e sem obreiros",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} mais"
"zone_join_more": "{a} + {n} mais",
"funnel_eq_title": "Como cada número é obtido",
"funnel_tip": "Este caderno é 1 de {n} listas UPG-100",
"life_kicker": "Perspectiva",
"life_title": "Uma vida humana, em semanas",
"life_lede": "Cem anos. Cinquenta e duas semanas cada. Cinco mil e duzentas casas — e é tudo. Cada semana em que você orar será uma destas.",
"life_foot": "100 anos x 52 semanas = 5.200 semanas.",
"life_weeks": "semanas",
"christian_kicker": "As mesmas 5.200 casas",
"christian_title": "Quem entre eles conhece a Cristo",
"christian_lede": "É a mesma grade — uma vida humana em semanas. Agora cada casa é uma parte das pessoas desta lista. As casas sombreadas são a proporção evangélica da sua população; o resto espera.",
"christian_reached": "Evangélicos",
"christian_lost": "Ainda esperando",
"christian_people": "pessoas",
"christian_foot": "Ponderado por população a partir do EvangelicalLevel de peoplegroups.org, tomando cada faixa publicada no seu ponto médio e 0 para «No Known Evangelicals»",
"pray_blessed": "Abençoei-os",
"pray_know": "O que saber",
"pray_do": "O que fazer"
},
"de": {
"map_page_kicker": "UNERREICHTE VOLKSGRUPPEN",
@ -892,7 +960,24 @@
"funnel_list_frontier-500k": "Projekt 700: Frontier-Volksgruppen mit 500.000 oder mehr",
"funnel_list_unengaged": "Unerreichte Volksgruppen ohne Mitarbeiter",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} weitere"
"zone_join_more": "{a} + {n} weitere",
"funnel_eq_title": "Wie jede Zahl zustande kommt",
"funnel_tip": "Dieses Heft ist 1 von {n} UPG-100-Listen",
"life_kicker": "Maßstab",
"life_title": "Ein Menschenleben, in Wochen",
"life_lede": "Hundert Jahre. Je zweiundfünfzig Wochen. Fünftausendzweihundert Kästchen — und das ist alles. Jede Woche, in der Sie beten, ist eines davon.",
"life_foot": "100 Jahre x 52 Wochen = 5.200 Wochen.",
"life_weeks": "Wochen",
"christian_kicker": "Dieselben 5.200 Kästchen",
"christian_title": "Wer unter ihnen Christus kennt",
"christian_lede": "Es ist dasselbe Raster — ein Menschenleben in Wochen. Jetzt steht jedes Kästchen für einen Anteil der Menschen auf dieser Liste. Die gefüllten Kästchen sind der evangelikale Anteil ihrer Bevölkerung; der Rest wartet.",
"christian_reached": "Evangelikal",
"christian_lost": "Warten noch",
"christian_people": "Menschen",
"christian_foot": "Bevölkerungsgewichtet aus dem EvangelicalLevel von peoplegroups.org, jede veröffentlichte Spanne in ihrer Mitte und 0 für „No Known Evangelicals“",
"pray_blessed": "Gesegnet",
"pray_know": "Was wissen",
"pray_do": "Was tun"
},
"it": {
"map_page_kicker": "GRUPPI ETNICI NON RAGGIUNTI",
@ -1069,7 +1154,24 @@
"funnel_list_frontier-500k": "Progetto 700: gruppi etnici di frontiera di 500.000 o più",
"funnel_list_unengaged": "Gruppi etnici non raggiunti e senza operai",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + altri {n}"
"zone_join_more": "{a} + altri {n}",
"funnel_eq_title": "Come si ottiene ogni numero",
"funnel_tip": "Questo fascicolo è 1 di {n} liste UPG-100",
"life_kicker": "Prospettiva",
"life_title": "Una vita umana, in settimane",
"life_lede": "Cento anni. Cinquantadue settimane ciascuno. Cinquemiladuecento caselle — ed è tutto. Ogni settimana in cui pregherai sarà una di queste.",
"life_foot": "100 anni x 52 settimane = 5.200 settimane.",
"life_weeks": "settimane",
"christian_kicker": "Le stesse 5.200 caselle",
"christian_title": "Chi tra loro conosce Cristo",
"christian_lede": "È la stessa griglia — una vita umana in settimane. Ora ogni casella è una parte delle persone di questa lista. Le caselle colorate sono la quota evangelica della loro popolazione; il resto attende.",
"christian_reached": "Evangelici",
"christian_lost": "Ancora in attesa",
"christian_people": "persone",
"christian_foot": "Ponderato per popolazione dall’EvangelicalLevel di peoplegroups.org, prendendo ogni fascia pubblicata al suo punto medio e 0 per «No Known Evangelicals»",
"pray_blessed": "Li ho benedetti",
"pray_know": "Cosa sapere",
"pray_do": "Cosa fare"
},
"ro": {
"map_page_kicker": "GRUPURI ETNICE NEATINSE",
@ -1246,7 +1348,24 @@
"funnel_list_frontier-500k": "Proiectul 700: grupuri etnice de frontieră de 500.000 sau mai multe",
"funnel_list_unengaged": "Grupuri etnice neatinse și fără lucrători",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + încă {n}"
"zone_join_more": "{a} + încă {n}",
"funnel_eq_title": "Cum se obține fiecare număr",
"funnel_tip": "Acest caiet este 1 din {n} liste UPG-100",
"life_kicker": "Perspectivă",
"life_title": "O viață de om, în săptămâni",
"life_lede": "O sută de ani. Câte cincizeci și două de săptămâni. Cinci mii două sute de căsuțe — și asta e tot. Fiecare săptămână în care te vei ruga este una dintre ele.",
"life_foot": "100 de ani x 52 de săptămâni = 5.200 de săptămâni.",
"life_weeks": "săptămâni",
"christian_kicker": "Aceleași 5.200 de căsuțe",
"christian_title": "Cine dintre ei Îl cunoaște pe Hristos",
"christian_lede": "Este aceeași grilă — o viață de om în săptămâni. Acum fiecare căsuță este o parte dintre oamenii de pe această listă. Căsuțele colorate sunt partea evanghelică a populației lor; restul așteaptă.",
"christian_reached": "Evanghelici",
"christian_lost": "Încă așteaptă",
"christian_people": "persoane",
"christian_foot": "Ponderat după populație pornind de la EvangelicalLevel de pe peoplegroups.org, fiecare interval publicat luat la mijloc și 0 pentru „No Known Evangelicals”",
"pray_blessed": "I-am binecuvântat",
"pray_know": "Ce să știu",
"pray_do": "Ce să fac"
},
"ru": {
"map_page_kicker": "НЕОХВАЧЕННЫЕ НАРОДЫ",
@ -1423,7 +1542,24 @@
"funnel_list_frontier-500k": "Проект 700: народы-фронтир численностью 500 000 и более",
"funnel_list_unengaged": "Неохваченные народы без работников",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + ещё {n}"
"zone_join_more": "{a} + ещё {n}",
"funnel_eq_title": "Как получено каждое число",
"funnel_tip": "Этот буклет — 1 из {n} списков UPG-100",
"life_kicker": "Масштаб",
"life_title": "Человеческая жизнь в неделях",
"life_lede": "Сто лет. По пятьдесят две недели. Пять тысяч двести клеток — и это всё. Каждая неделя, когда вы молитесь, — одна из них.",
"life_foot": "100 лет x 52 недели = 5200 недель.",
"life_weeks": "недель",
"christian_kicker": "Те же 5200 клеток",
"christian_title": "Кто среди них знает Христа",
"christian_lede": "Это та же сетка — человеческая жизнь в неделях. Теперь каждая клетка — доля людей из этого списка. Закрашенные клетки — евангельская доля их населения; остальные ждут.",
"christian_reached": "Евангельские",
"christian_lost": "Всё ещё ждут",
"christian_people": "человек",
"christian_foot": "Взвешено по населению на основе EvangelicalLevel с peoplegroups.org: каждый опубликованный диапазон берётся по середине, «No Known Evangelicals» — как 0",
"pray_blessed": "Благословил",
"pray_know": "Что узнать",
"pray_do": "Что сделать"
},
"ar": {
"map_page_kicker": "شعوب غير مبلَّغة",
@ -1600,7 +1736,24 @@
"funnel_list_frontier-500k": "مشروع 700: المجموعات الشعبية الحدودية التي يبلغ عددها 500,000 أو أكثر",
"funnel_list_unengaged": "مجموعات شعبية غير مبلَّغة وبلا عاملين",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} أخرى"
"zone_join_more": "{a} + {n} أخرى",
"funnel_eq_title": "كيف نصل إلى كل رقم",
"funnel_tip": "هذه الحزمة واحدة من {n} قائمة UPG-100",
"life_kicker": "منظور",
"life_title": "حياة إنسان، بالأسابيع",
"life_lede": "مئة عام. اثنان وخمسون أسبوعًا في كل عام. خمسة آلاف ومئتا مربّع — وهذا كل شيء. كل أسبوع تصلّي فيه هو واحد منها.",
"life_foot": "١٠٠ سنة × ٥٢ أسبوعًا = ٥٢٠٠ أسبوع.",
"life_weeks": "أسبوعًا",
"christian_kicker": "المربّعات الـ٥٢٠٠ نفسها",
"christian_title": "مَن منهم يعرف المسيح",
"christian_lede": "إنها الشبكة نفسها — حياة إنسان بالأسابيع. الآن كل مربّع يمثّل نصيبًا من الناس في هذه القائمة. المربّعات الملوّنة هي النسبة الإنجيلية من سكانهم؛ والبقية ينتظرون.",
"christian_reached": "إنجيليون",
"christian_lost": "ما زالوا ينتظرون",
"christian_people": "نسمة",
"christian_foot": "مرجّح بالسكان من EvangelicalLevel على peoplegroups.org، مع أخذ كل نطاق منشور عند نقطة منتصفه و٠ لـ«No Known Evangelicals»",
"pray_blessed": "باركتهم",
"pray_know": "ماذا أعرف",
"pray_do": "ماذا أفعل"
},
"hi": {
"map_page_kicker": "अनछुए जनसमूह",
@ -1777,7 +1930,24 @@
"funnel_list_frontier-500k": "परियोजना 700: 500,000 या अधिक के सीमांत जनसमूह",
"funnel_list_unengaged": "अप्राप्त जनसमूह जिनमें कोई कार्यकर्ता नहीं",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + {n} और"
"zone_join_more": "{a} + {n} और",
"funnel_eq_title": "हर संख्या कैसे प्राप्त होती है",
"funnel_tip": "यह पुस्तिका {n} UPG-100 सूचियों में से 1 है",
"life_kicker": "परिप्रेक्ष्य",
"life_title": "एक मानव जीवन, सप्ताहों में",
"life_lede": "सौ वर्ष। हर वर्ष बावन सप्ताह। पाँच हज़ार दो सौ खाने — और बस इतना ही। जिस भी सप्ताह आप प्रार्थना करेंगे, वह इन्हीं में से एक है।",
"life_foot": "100 वर्ष x 52 सप्ताह = 5,200 सप्ताह।",
"life_weeks": "सप्ताह",
"christian_kicker": "वही 5,200 खाने",
"christian_title": "उनमें से कौन मसीह को जानता है",
"christian_lede": "यह वही ग्रिड है — सप्ताहों में एक मानव जीवन। अब हर खाना इस सूची के लोगों का एक हिस्सा है। रंगे हुए खाने उनकी जनसंख्या का सुसमाचारी हिस्सा हैं; बाकी प्रतीक्षा कर रहे हैं।",
"christian_reached": "सुसमाचारी",
"christian_lost": "अब भी प्रतीक्षा में",
"christian_people": "लोग",
"christian_foot": "peoplegroups.org के EvangelicalLevel से जनसंख्या-भारित, हर प्रकाशित श्रेणी को उसके मध्यबिंदु पर और «No Known Evangelicals» को 0 मानते हुए",
"pray_blessed": "आशीष दी",
"pray_know": "क्या जानना है",
"pray_do": "क्या करना है"
},
"zh": {
"map_page_kicker": "未得之民",
@ -1954,6 +2124,23 @@
"funnel_list_frontier-500k": "700 计划:50 万人以上的前沿族群",
"funnel_list_unengaged": "无人服事的未得之民族群",
"zone_join": "{a} + {b}",
"zone_join_more": "{a} + 另外 {n} 个"
"zone_join_more": "{a} + 另外 {n} 个",
"funnel_eq_title": "每个数字是如何得出的",
"funnel_tip": "本手册是 {n} 份 UPG-100 名单中的 1 份",
"life_kicker": "视角",
"life_title": "一生,以周计",
"life_lede": "一百年。每年五十二周。五千二百个格子——就这些。你祷告的每一周都是其中之一。",
"life_foot": "100 年 x 52 周 = 5,200 周。",
"life_weeks": "周",
"christian_kicker": "同样的 5,200 个格子",
"christian_title": "他们中谁认识基督",
"christian_lede": "这是同一张网格——以周计的一生。现在每个格子代表这份名单中人口的一部分。着色的格子是他们人口中的福音派比例;其余的仍在等待。",
"christian_reached": "福音派",
"christian_lost": "仍在等待",
"christian_people": "人",
"christian_foot": "依据 peoplegroups.org 的 EvangelicalLevel 按人口加权,每个已公布区间取其中点,\"No Known Evangelicals\" 计为 0",
"pray_blessed": "已祝福",
"pray_know": "当知道什么",
"pray_do": "当做什么"
}
}

View file

@ -124,6 +124,17 @@ const { mapPage, listPages, pointsPage, wheelPage, css, mapCss, PER_PAGE, W, H }
// ── DATA ────────────────────────────────────────────────────────────────────
const clusters = JSON.parse(readFileSync(join(DATA, 'clusters.json'), 'utf8'));
// ── THE CHRISTIAN-SHARE PAGE (packet page 3) ────────────────────────────────
// READ, NEVER COMPUTED HERE. data/people-groups/christian-share.json is generated from the IMB CSV
// joined to DOXA's snapshot on ROP3+country — the same join the Vision Card uses — and it carries its
// own _method and provenance. Re-deriving the percentage in this file would give one fact two sources
// that drift apart, which is the mistake the funnel page already documents for its four steps.
// A missing file yields null and buildPacket omits page 3 rather than printing a number it could not read.
let CHRISTIAN = null;
try {
CHRISTIAN = JSON.parse(readFileSync(join(__dirname, '../../../data/people-groups/christian-share.json'), 'utf8'));
} catch (_) { /* page 3 is omitted, not faked */ }
// ── THE PAGE-2 FUNNEL ───────────────────────────────────────────────────────
// The same context the browser path builds in upg-100-list.vue's packetFunnel(), so the Node
// generator and the in-page print cannot disagree about page 2 any more than they can about the
@ -153,10 +164,15 @@ function buildFunnel(membership) {
// resolve, and clusters.json's two DEAF lists are a second, overlapping partition that
// allLists() excludes. "In no list" has no single answer until someone says which partition is
// meant. Both paths must agree, so this one stops claiming it too.
const lists = [...(clusters.lists || []), ...(clusters.asia_lists || []),
...(clusters.uncovered_lists || [])];
// ⚠ COUNT THROUGH leafZones, NOT THE THREE CLUSTER KEYS. Spreading lists/asia_lists/
// uncovered_lists gives 27 and silently omits `deaf`, whose two deaf_lists entries
// buildLegendNodes collapses into ONE unified leaf — legend-tree.js:106 states the arithmetic
// ("13 + 12 + 2 + 1 deaf = 28"). The packet's triangle tip prints this number as "1 of N", so a
// 27 there is a visible wrong fact on the page. Same omission as allZones() had before 3ef164c0;
// this was the last copy of it.
const listCount = allZones().length;
const listTotal = Number(raw.steps[raw.steps.length - 1].groups) || 0;
return { ...raw, listCount: lists.length, listTotal };
return { ...raw, listCount, listTotal };
} catch (_) { return null; }
}
const { evaluate } = await import(join(DATA, 'selector-evaluator.js'));
@ -289,7 +305,7 @@ async function fetchPhotos(groups, limit = 16) {
// the shared renderer, so the two packets cannot drift.
async function buildPacket(zone, groups, mapSvg) {
const photos = await fetchPhotos(groups);
const html = R.buildPacket(zone, groups, photos, mapSvg, FUNNEL);
const html = R.buildPacket(zone, groups, photos, mapSvg, FUNNEL, CHRISTIAN);
const withPhoto = groups.filter((g) => photos.has(g.slug)).length;
// ★ COUNT THE SHEETS IN THE EMITTED HTML, do not re-derive them from a formula. The old line
// computed `1 + ceil(n/PER_PAGE) + 2` while its own comment claimed the number was "asked of the

View file

@ -244,7 +244,7 @@
"dev": "bun x vite",
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",

View file

@ -127,9 +127,25 @@ function paneElement(pane) {
return el
}
// `inert` on every pane except the active one: the same contract as upg-100-mfe (4642e78d). The panes
// are stacked in one scroll-snap column, so the hidden pane is still full-size geometry; inert tells
// PosterWall (67e9ede5) the wall is out of view so it forgets its shown card, and a second
// "Show on map" after Map -> back SHOWS the country again instead of toggling it off (rega row 79).
// inert, not display:none: hiding would tear down the live map.
function markActivePane(activeId) {
if (!column || !activeId) return
for (const pane of column.panes) {
const el = column.mountEl(pane.id)
if (!el) continue
if (pane.id === activeId) el.removeAttribute('inert')
else el.setAttribute('inert', '')
}
}
function go(id) {
view.value = id
column?.scrollTo(id)
markActivePane(id) // now, not after the smooth scroll settles
}
// ── tab 1 -> tab 2 (shell seam) ─────────────────────────────────────────────
@ -183,9 +199,11 @@ onMounted(() => {
column.mountEl(pane.id)?.appendChild(el)
}
// Tabs are a readout as well as a control — follow the user's scroll.
markActivePane(column.current() || view.value) // the fresh-load state, before any switch
offScroll = column.onScroll(() => {
const cur = column.current()
if (cur && cur !== view.value) view.value = cur
if (cur) markActivePane(cur) // always: a switcher-driven scroll already set view.value
})
window.addEventListener(SHOW_ON_MAP, onShowOnMap)
// profile-config `view` opens a chosen pane ("map" or "wall"), so an embed can start on the map.

View file

@ -1,6 +1,7 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { tStatic } from '@map/i18n/index.js'
/**
* pplr-coverage.vue — Country choropleth: avg # of locales filled per PG in that country.
*
@ -56,7 +57,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}
map.value.on('load', async () => {

View file

@ -1,6 +1,7 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn, setProjectionFor } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { tStatic } from '@map/i18n/index.js'
/**
* pplr-language-families.vue — PG pins colored by PPLR ethnolinguistic family.
*
@ -432,7 +433,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}
if (map.value) {

View file

@ -7,6 +7,7 @@
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { tStatic } from '@map/i18n/index.js'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { addCountryPolygons, addPGPoints, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
import LoadingGlobe from '../components/LoadingGlobe.vue'
@ -40,7 +41,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}
map.value.on('load', async () => {

View file

@ -1,6 +1,8 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn, setProjectionFor } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { religionFamilyLabel } from '@map/colors/religion.js'
import { tStatic } from '@map/i18n/index.js'
/**
* pplr-religion.vue — PG pins colored by primary religion.
*
@ -137,6 +139,12 @@ const RELIGION_GROUPS = [
{ key: 'other', label: 'Other / Unknown', color: '#4b5563' }, // dark gray
]
// Shown in the reader's language through the shared religion catalogue; the key and the filter stay
// English. "Other / Unknown" is one bucket here, so it is named from both catalogue entries.
const groupLabel = (g) => (g.key === 'other'
? `${religionFamilyLabel('Other')} / ${religionFamilyLabel('Unknown')}`
: religionFamilyLabel(g.label))
const COLOR_EXPR = [
'match', ['get', 'rg'],
...RELIGION_GROUPS.flatMap(g => [g.key, g.color]),
@ -147,7 +155,7 @@ const COLOR_EXPR = [
const religionCounts = ref({})
const religionLegendItems = computed(() => RELIGION_GROUPS.map(g => ({
key: g.key,
label: g.label,
label: groupLabel(g),
color: g.color,
count: religionCounts.value[g.key] || 0,
filter: ['==', ['get', 'rg'], g.key],
@ -166,7 +174,7 @@ function onLegendSelect(node) {
const religionLocalIndex = computed(() => {
const out = RELIGION_GROUPS.map((g, i) => ({
id: `rel:${g.key}`,
label: g.label,
label: groupLabel(g),
color: g.color,
kind: 'religion',
depth: 0,
@ -250,7 +258,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}
if (map.value) {
@ -279,7 +287,7 @@ onMounted(async () => {
religionCounts.value = counts
const dominant = RELIGION_GROUPS.reduce((a, b) => (counts[a.key] || 0) > (counts[b.key] || 0) ? a : b)
// Per user 2026-05-02: drop the "Ready" prefix — it was unclear and chewed up width.
status.value = t('pplr.status.readyReligion', { pins: pgData.value.features.length.toLocaleString(), religion: dominant.label, count: (counts[dominant.key]||0).toLocaleString() })
status.value = t('pplr.status.readyReligion', { pins: pgData.value.features.length.toLocaleString(), religion: groupLabel(dominant), count: (counts[dominant.key]||0).toLocaleString() })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true

View file

@ -1,6 +1,7 @@
<script setup>
import { dataUrl, glFor, exposeMap, setProjectionFor } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { tStatic } from '@map/i18n/index.js'
/**
* pplr-rop-tree.vue — ROP hierarchy: Affinity Bloc → Cluster → People Group → PGIC.
*
@ -399,7 +400,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}

View file

@ -15,6 +15,7 @@
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { tStatic } from '@map/i18n/index.js'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS, escHtml } from '../utils/pplrMap.js'
import LoadingGlobe from '../components/LoadingGlobe.vue'
@ -48,7 +49,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}
map.value.on('load', async () => {

View file

@ -1,6 +1,7 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { tStatic } from '@map/i18n/index.js'
/**
* pplr-translations.vue — PG name in all 17 locales.
*
@ -62,7 +63,7 @@ onMounted(async () => {
if (!map.value) {
// The map did not construct (e.g. provider=mapbox with no token; useMapInstance logs why). Say so
// and stop: everything below needs a map, and map.value.on() threw into the mounted hook.
status.value = t('messages.mapUnavailable')
status.value = tStatic('messages.mapUnavailable') // a shared key: usePplrT would prefix it with pplr.
return
}
if (map.value) {

View file

@ -0,0 +1,55 @@
# rudder-voice — Rudder, private voice-to-text (desktop target)
**Rudder is a plugin that isn't a map.** It shows that the same dx-maps pattern — one folder in
`plugin-designs/`, developed live in the Plugin Studio — can ship a **desktop app with its own local
backend**, not only embeddable maps. It is also a genuinely useful tool for the people who build here:
talk to your agents instead of typing, with nothing leaving your computer.
- **Streaming.** Words appear in the text box while you speak; talk for as long as you like.
- **Type and talk at once.** Type, paste links or code anywhere in the box; everything you say is
added to the end. While you're typing, Rudder holds its words and adds them when you pause.
- **CPU only.** No graphics card. About 1 GB of memory; keeps up with normal speech on a laptop CPU.
- **Offline, no telemetry.** NVIDIA Nemotron 3.5 ASR Streaming (0.6B) run by NVIDIA's open-source
NeMo-Speech.cpp (ggml). Traced with `strace` while transcribing: zero sockets, zero DNS lookups.
- **Microphones like a video-call app.** Pick the input, hot-plug updates, a 3-second mic test, and
plain-language errors (muted, disconnected, dead Bluetooth mic link — which it tries to recover).
## Run it in the Plugin Studio
```bash
bash backend/rudder-engine/setup-runtime.sh # once: runtime + model (verified by SHA-256) + engine build
bun run dev # starts the engine too; open "rudder-voice" in the studio
```
Linux with PipeWire or PulseAudio (`parec`, `pactl`). The engine listens on `127.0.0.1` only.
## How it fits together
```
<rudder-voice> (this folder) ──HTTP + SSE /api/rudder──► backend/rudder-engine (Go)
├─ parec / pw-record (mic, open only while recording)
└─ WebSocket ► nemo-speech serve (127.0.0.1, model stays loaded)
```
`bun run dev` starts `backend/bin/rudder-engine` through `internal/backend-dev-plugin.mjs` and forwards
`/api/rudder` to it, so the plugin calls the same API in the browser as it will inside the desktop app,
where Tauri runs the engine as a sidecar and passes its address in `profile-config` (`apiBase`).
`rudder-voice.meta.json` declares `"targets": ["desktop"]`: on a plain website there is no local engine,
so this plugin is not meant for web embeds.
## Settings (environment variables)
| Variable | Default | What it does |
|---|---|---|
| `RUDDER_PORT` | `5193` | engine HTTP port (loopback) |
| `RUDDER_NEMO_PORT` | `5194` | speech server port (loopback) |
| `RUDDER_MODEL` / `RUDDER_NEMO_BIN` | `~/.local/share/rudder/...` | model and runtime paths; the engine refuses a model that isn't a local file |
| `RUDDER_LANG` | `en-US` | default language (the model supports ~30) |
| `RUDDER_KEEP_MSBC` | unset | keep Bluetooth headsets on mSBC instead of switching them to the more stable CVSD |
| `RUDDER_BT_RESET` | unset | optional command that fully resets a Bluetooth headset, used when its mic link dies |
## Not yet
- Desktop build (`desktop:build`) with the engine as a Tauri sidecar.
- A saved word-correction table (PocketBase) for technical terms the model mishears.

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>doxa-maps · rudder-voice staging</title>
<link rel="icon" href="data:,">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: #0b0f14; color: #e5e7eb; min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
.wrap { max-width: 960px; margin: 0 auto; padding: 28px 20px 48px; }
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.sub { color: #9ca3af; font-size: 13px; margin: 6px 0 24px; max-width: 820px; }
.sub code { color: #c4b5fd; }
.host { border: 1px solid #1f2937; border-radius: 12px; overflow: hidden; }
rudder-voice { display: block; width: 100%; }
</style>
</head>
<body>
<div class="wrap">
<h1>rudder-voice · staging</h1>
<p class="sub">Desktop-target plugin: private, offline, streaming voice-to-text. The speech model runs in the
local backend <code>backend/rudder-engine</code> (NVIDIA NeMo-Speech.cpp + Nemotron 3.5 ASR), started by
<code>bun run dev</code> and reached at <code>/api/rudder</code>.</p>
<div class="host">
<rudder-voice></rudder-voice>
</div>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>

View file

@ -0,0 +1,23 @@
/**
* rudder-voice / index.js — Bundle entry (Capability-1 single element, DESKTOP target).
*
* <rudder-voice> — private, offline, streaming voice-to-text. The UI is this bundle; the speech model
* runs in the local backend (backend/rudder-engine), started by `bun run dev` in the Plugin Studio and
* by Tauri as a sidecar in the desktop app. See rudder-voice.vue for the contract.
*
* Desktop-only: rudder-voice.meta.json declares "targets": ["desktop"]. On a plain website there is no
* local engine, so the element shows "Engine offline" instead of pretending to work.
*
* Usage (desktop shell / Plugin Studio):
* <rudder-voice profile-config='{"apiBase":"http://127.0.0.1:5193/api/rudder"}'></rudder-voice>
*/
import { defineCustomElement } from 'vue'
import RudderVoice from './rudder-voice.vue'
const RudderVoiceElement = defineCustomElement(RudderVoice)
if (typeof customElements !== 'undefined') {
if (!customElements.get('rudder-voice')) customElements.define('rudder-voice', RudderVoiceElement)
}
export default RudderVoiceElement

View file

@ -0,0 +1,7 @@
{
"_comment": "Rudder — private, offline, streaming voice-to-text. DESKTOP target: the speech model runs in the local backend (backend/rudder-engine: NVIDIA NeMo-Speech.cpp + Nemotron 3.5 ASR GGUF), started by `bun run dev` and run by Tauri as a sidecar. Not for public web embeds: a website has no local engine.",
"type": "single",
"label": "Rudder — voice to text (desktop)",
"targets": ["desktop"],
"backend": "backend/rudder-engine"
}

View file

@ -0,0 +1,650 @@
<script setup>
/**
* rudder-voice.vue — Rudder: private, offline, streaming voice-to-text.
*
* @element rudder-voice
* @description A DESKTOP-target plugin (rudder-voice.meta.json "targets": ["desktop"]). The speech
* model runs in the local backend (backend/rudder-engine → NVIDIA NeMo-Speech.cpp + Nemotron 3.5
* ASR), never in the browser and never on a server. In the Plugin Studio, `bun run dev` starts that
* backend and forwards /api/rudder; in the desktop app, Tauri runs it as a sidecar and passes its
* address in `profile-config` {"apiBase": "http://127.0.0.1:<port>/api/rudder"}.
*
* Everything is agent-interfaceable (the old Rudder's prime directive): the 🐞 panel copies the
* engine log + status with a header telling an agent what to do with it.
*/
import { computed, onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
defineOptions({ inheritAttrs: false })
const props = defineProps({ profileConfig: { type: String, default: '' } })
const cfg = computed(() => {
try { return props.profileConfig ? JSON.parse(props.profileConfig) : {} } catch { return {} }
})
const apiBase = computed(() => (cfg.value.apiBase || '/api/rudder').replace(/\/$/, ''))
const LANGUAGES = [
['en-US', 'English (US)'], ['en-GB', 'English (UK)'], ['es-US', 'Spanish (US)'], ['es-ES', 'Spanish (Spain)'],
['fr-FR', 'French'], ['de-DE', 'German'], ['it-IT', 'Italian'], ['pt-BR', 'Portuguese (Brazil)'],
['nl-NL', 'Dutch'], ['ru-RU', 'Russian'], ['uk-UA', 'Ukrainian'], ['tr-TR', 'Turkish'], ['ar-AR', 'Arabic'],
['hi-IN', 'Hindi'], ['ja-JP', 'Japanese'], ['ko-KR', 'Korean'], ['vi-VN', 'Vietnamese'], ['zh-CN', 'Mandarin'],
]
const STORE_KEY = 'rudder-voice:v1'
function load() {
try { return JSON.parse(localStorage.getItem(STORE_KEY) || '{}') } catch { return {} }
}
function save() {
try { localStorage.setItem(STORE_KEY, JSON.stringify({ language: language.value, device: device.value, draft: draft.value })) } catch {}
}
const saved = load()
const engine = ref('connecting') // connecting | starting | ready | error | offline
const engineError = ref('')
const runtime = ref('')
const model = ref('')
const recording = ref(false)
const busy = ref(false)
const language = ref(saved.language || cfg.value.language || 'en-US')
// The draft: a text area you type and paste into; every finished utterance is APPENDED to its end.
// (v1 kept a list of lines — migrate it so nothing already dictated is lost.)
const draft = ref(typeof saved.draft === 'string' ? saved.draft
: Array.isArray(saved.lines) ? saved.lines.map((l) => l.text).join(' ') : '')
const partial = ref('') // the utterance being heard right now (streamed into the box's live tail)
const box = ref(null)
const level = ref(0)
const notice = ref('')
const noticeKind = ref('info')
const copied = ref('')
const showDebug = ref(false)
const showInfo = ref(false)
const clientLog = ref([])
// ─── Microphones (the Jitsi device-selection pattern, served natively by the engine) ───
const devices = ref([]) // [{id,label,bus,default,muted,volume,state}]
const offCards = ref([]) // sound cards with a mic that are switched off (e.g. the laptop's own)
const deviceError = ref('')
const device = ref(saved.device || '') // '' = follow the system default input
const testing = ref(false)
const testResult = ref(null) // {ok, verdict, peak}
const BUS_ICON = { bluetooth: '🎧', usb: '🔌', pci: '💻' }
function deviceLabel(d) {
const bits = [(BUS_ICON[d.bus] || '🎙') + ' ' + d.label]
if (d.muted) bits.push('(muted)')
else if (d.volume) bits.push('· ' + d.volume)
return bits.join(' ')
}
const defaultDevice = computed(() => devices.value.find((d) => d.default))
const chosen = computed(() => devices.value.find((d) => d.id === device.value) || (device.value ? null : defaultDevice.value))
function applyDevices(list) {
if (!list) return
devices.value = list.devices || []
offCards.value = list.offCards || []
deviceError.value = list.error || ''
// A saved choice that is gone (headset switched off) falls back to the default, and says so.
if (device.value && !devices.value.some((d) => d.id === device.value)) {
if (!recording.value) {
say('Your chosen microphone is no longer connected — using the system default instead.', 'warn')
device.value = ''
save()
}
}
}
async function refreshDevices() {
try { applyDevices(await (await fetch(apiBase.value + '/devices')).json()) } catch (err) { logc('devices fetch failed: ' + err) }
}
async function testMic() {
if (testing.value || recording.value) return
testing.value = true
testResult.value = null
try {
testResult.value = await post('/test', { device: device.value })
} catch (err) {
testResult.value = { ok: false, verdict: String(err.message || err) }
} finally {
testing.value = false
level.value = 0
}
}
let es = null
let retry = null
function logc(msg) {
clientLog.value.push(new Date().toISOString().slice(11, 23) + ' ' + msg)
if (clientLog.value.length > 200) clientLog.value.splice(0, clientLog.value.length - 200)
}
function say(msg, kind = 'info') { notice.value = msg; noticeKind.value = kind }
// ─── Writing into the box ────────────────────────────────────────────────────────────────────────
// Every word streams straight into the END of the box while you keep typing, pasting and editing
// anywhere. The sentence in progress lives in a "live tail" — the last `live.text.length` characters
// of the draft — and each new word rewrites only that tail.
//
// ★ TYPING WHILE TALKING (bug fixed 2026-09-27): once you type into or after the tail it is yours, and
// Rudder must continue with ONLY the words it hasn't written yet. v1 re-wrote the whole sentence-so-far
// after every keystroke, so each key press duplicated the utterance. Now `written` counts the words of
// this utterance already in the box, and the finished (punctuated) sentence is lined up word-by-word
// against them, so nothing is ever written twice.
let live = null // { start, text, shown } — the tail Rudder may still rewrite; shown = words in it
let written = 0 // words of the current utterance committed to the box before the current tail
// The tail is still Rudder's while the box ENDS with it — wherever it now starts. Typing or pasting
// anywhere above it shifts its position, and that must not count as touching it (bug 2026-09-27: a
// position-based check released the tail on every keystroke above it, cutting "speaking" to "spe").
function liveIntact() {
if (!live || !draft.value.endsWith(live.text)) return false
live.start = draft.value.length - live.text.length
return true
}
// Change only the end of the draft, keeping the person's caret/selection and scroll where they were.
function writeEnd(next) {
const el = box.value
const focused = el && el.getRootNode().activeElement === el
const selStart = el ? el.selectionStart : 0
const selEnd = el ? el.selectionEnd : 0
const atBottom = !el || el.scrollHeight - el.scrollTop - el.clientHeight < 32
draft.value = next
save()
nextTick(() => {
if (!el) return
if (focused) el.setSelectionRange(Math.min(selStart, next.length), Math.min(selEnd, next.length))
if (atBottom || !focused) el.scrollTop = el.scrollHeight
})
}
const sepFor = (text) => (!text || /\s$/.test(text) ? '' : ' ')
const wordsOf = (text) => (text || '').trim().split(/\s+/).filter(Boolean)
// The person typed into or after the tail, so it's theirs now. Count its words as written — except a
// HALF-FINISHED last word: the model streams words in pieces ("for" … "tnight"), and typing between the
// pieces used to freeze "for" and lose the rest (measured 2026-09-27: "for a for [typed]"). If the word
// has since grown, the fragment is removed (it sits just before the person's text; their caret is
// shifted so it stays put relative to what they typed) and the whole word is written after it.
const norm = (x) => (x || '').toLowerCase().replace(/[^\p{L}\p{N}']/gu, '')
function releaseTail(utteranceWords) {
const shownWords = wordsOf(live.text)
const lastShown = shownWords[shownWords.length - 1] || ''
const nowWord = utteranceWords[written + live.shown - 1]
const grew = live.shown > 0 && nowWord !== undefined && norm(nowWord) !== norm(lastShown) && norm(nowWord).startsWith(norm(lastShown))
// Find the tail by its text: typing above it may have moved it since it was written.
const found = draft.value.lastIndexOf(live.text)
if (found >= 0) live.start = found
const tailEnd = live.start + live.text.length
const prefixOk = found >= 0
if (grew && prefixOk && lastShown) {
let fragStart = tailEnd - lastShown.length
if (fragStart > live.start && draft.value[fragStart - 1] === ' ') fragStart--
const cut = tailEnd - fragStart
const el = box.value
const focused = el && el.getRootNode().activeElement === el
const shift = (i) => (i >= tailEnd ? i - cut : i > fragStart ? fragStart : i)
const a = el ? shift(el.selectionStart) : 0
const b = el ? shift(el.selectionEnd) : 0
draft.value = draft.value.slice(0, fragStart) + draft.value.slice(tailEnd)
if (focused) nextTick(() => el.setSelectionRange(a, b))
written += live.shown - 1
} else {
written += live.shown
}
live = null
}
// Write `words` as the live tail: rewrite it if it's still ours, otherwise open a new one at the end.
function writeTail(words, utterance) {
const t = words.join(' ')
if (liveIntact()) {
const lead = live.text.match(/^\s*/)[0]
live.text = t ? lead + t : ''
live.shown = words.length
writeEnd(draft.value.slice(0, live.start) + live.text)
return
}
if (live) releaseTail(utterance) // the person took over the old tail: keep it
if (!t) return
live = { start: draft.value.length, text: sepFor(draft.value) + t, shown: words.length }
writeEnd(draft.value + live.text)
}
// ─── Hold the stream while the person is typing ─────────────────────────────────────────────────
// Typing and streaming both write at the end of the box; a word landing between two keystrokes splices
// into what is being typed (measured 2026-09-27: "he'[typed]ll"). So while keys are being pressed the
// stream is buffered, and flushed — in order, nothing dropped — 700 ms after the last keystroke.
const TYPING_HOLD_MS = 700
let lastTypedAt = 0
let pendingPartial = null
const pendingFinals = []
let flushTimer = null
function markTyping() { lastTypedAt = Date.now() }
function onUserInput() {
markTyping()
save()
}
function typingNow() { return Date.now() - lastTypedAt < TYPING_HOLD_MS }
function scheduleFlush() {
clearTimeout(flushTimer)
flushTimer = setTimeout(() => {
if (typingNow()) return scheduleFlush()
const finals = pendingFinals.splice(0)
const p = pendingPartial
pendingPartial = null
for (const f of finals) finishUtterance(f)
if (p !== null) streamPartial(p)
}, TYPING_HOLD_MS)
}
function onPartial(full) {
if (typingNow() || pendingFinals.length) { pendingPartial = full; scheduleFlush(); return }
streamPartial(full)
}
function onFinal(text) {
if (typingNow() || pendingFinals.length) { pendingFinals.push(text); pendingPartial = null; scheduleFlush(); return }
finishUtterance(text)
}
// A word arrived: show the words of this utterance that aren't in the box yet.
function streamPartial(full) {
const w = wordsOf(full)
if (live && !liveIntact()) releaseTail(w)
writeTail(w.slice(written), w)
}
// The sentence is finished: the same span of words, now punctuated and capitalised.
function finishUtterance(text) {
const w = wordsOf(text)
if (live && !liveIntact()) releaseTail(w)
writeTail(w.slice(written), w)
live = null
written = 0
}
function applyStatus(s) {
if (!s) return
if (s.engine) engine.value = s.engine
engineError.value = s.error || ''
if (s.runtime) runtime.value = s.runtime
if (s.model) model.value = s.model
if (typeof s.recording === 'boolean') recording.value = s.recording
}
function connect() {
clearTimeout(retry)
try { es?.close() } catch {}
es = new window.EventSource(apiBase.value + '/events')
es.onopen = () => logc('events connected')
es.onmessage = (m) => {
let e
try { e = JSON.parse(m.data) } catch { return }
switch (e.type) {
case 'status':
applyStatus(e.status || e)
if (e.engine === 'error') say(e.error || 'Speech engine error', 'error')
if (e.engine === 'ready' && noticeKind.value !== 'error') say('')
break
case 'recording':
recording.value = !!e.recording
if (!e.recording) { level.value = 0; flushPartial() }
break
case 'partial':
partial.value += e.text
onPartial(partial.value)
break
case 'final':
partial.value = ''
onFinal(e.text)
break
case 'recovering':
say(e.message, 'warn')
break
case 'recovered':
say(e.message, 'ok')
setTimeout(() => { if (noticeKind.value === 'ok') say('') }, 4000)
break
case 'devices':
applyDevices(e.devices)
break
case 'level':
level.value = Math.min(1, (e.peak || 0) * 3)
break
case 'warning':
say(e.message, 'warn')
break
case 'error':
say(e.message, 'error')
logc('engine error: ' + e.message)
break
}
}
es.onerror = () => {
engine.value = 'offline'
recording.value = false
logc('events disconnected; retrying in 3s')
try { es.close() } catch {}
retry = setTimeout(connect, 3000)
}
}
// Recording stopped mid-sentence: the streamed words are already in the box, so just let go of them.
function flushPartial() {
clearTimeout(flushTimer)
const finals = pendingFinals.splice(0)
const p = pendingPartial
pendingPartial = null
for (const f of finals) finishUtterance(f)
if (p !== null) streamPartial(p)
partial.value = ''
live = null
written = 0
written = 0
}
async function post(path, body) {
const r = await fetch(apiBase.value + path, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}),
})
const j = await r.json().catch(() => ({}))
if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`)
return j
}
async function toggle() {
if (busy.value) return
busy.value = true
try {
if (recording.value) {
await post('/stop')
} else {
say('')
testResult.value = null
applyStatus(await post('/start', { language: language.value, device: device.value }))
}
} catch (err) {
say(String(err.message || err), 'error')
logc('toggle failed: ' + err)
} finally {
busy.value = false
}
}
const words = computed(() => (draft.value.trim() ? draft.value.trim().split(/\s+/).length : 0))
async function copy(text, what) {
try {
await navigator.clipboard.writeText(text)
} catch {
const ta = document.createElement('textarea')
ta.value = text; document.body.appendChild(ta); ta.select()
try { document.execCommand('copy') } catch {}
ta.remove()
}
copied.value = what
setTimeout(() => { if (copied.value === what) copied.value = '' }, 1500)
}
function clearAll() {
if (draft.value.trim() && !window.confirm('Clear the whole text box? This cannot be undone.')) return
draft.value = ''
partial.value = ''
live = null
save()
}
// ★ PRIVACY: the report is meant to be pasted to an agent, so it describes the setup without
// identifying the person: no browser fingerprint (user-agent), no device names ("Anna's AirPods"), no
// device IDs (they embed the Bluetooth MAC address), and never the text in the box — only its length.
// The engine log it includes is already redacted at the source (MACs, home folder).
const MAC_RE = /\b([0-9a-f]{2}[:_]){5}[0-9a-f]{2}\b/gi
function privateDevices() {
return devices.value.map((d, i) => ({ mic: 'mic ' + (i + 1), bus: d.bus, default: d.default, muted: d.muted, volume: d.volume, state: d.state }))
}
function browserFamily() {
const ua = navigator.userAgent
return /firefox/i.test(ua) ? 'Firefox' : /edg\//i.test(ua) ? 'Edge' : /chrom/i.test(ua) ? 'Chromium-based' : /safari/i.test(ua) ? 'Safari' : 'other'
}
async function copyDebug() {
let engineLog = '(unreachable)'
let status = '(unreachable)'
try { engineLog = await (await fetch(apiBase.value + '/log')).text() } catch {}
try { status = JSON.stringify(await (await fetch(apiBase.value + '/status')).json(), null, 2) } catch {}
const text = [
'// RUDDER DEBUG REPORT — for a coding agent.',
'// Read the engine status and log below, find why transcription failed or misbehaved, and propose a fix',
'// in backend/rudder-engine/main.go or plugin-designs/rudder-voice/rudder-voice.vue. Do not guess:',
'// quote the log lines that prove the cause.',
'',
'== status ==', status,
'== plugin ==', JSON.stringify({ apiBase: apiBase.value, draftChars: draft.value.length, engine: engine.value, recording: recording.value, language: language.value, device: device.value ? 'chosen mic' : '(system default)', browser: browserFamily() }, null, 2),
'== microphones ==', JSON.stringify({ devices: privateDevices(), offCards: offCards.value.length, error: deviceError.value, lastTest: testResult.value }, null, 2),
'== client log ==', clientLog.value.join('\n'),
'== engine log ==', engineLog,
].join('\n').replace(MAC_RE, 'XX:XX:XX:XX:XX:XX')
copy(text, 'debug')
}
function onKey(ev) {
// Ctrl+Shift+Space toggles recording without reaching for the mouse.
if (ev.ctrlKey && ev.shiftKey && ev.code === 'Space') { ev.preventDefault(); toggle() }
}
onMounted(() => {
connect()
refreshDevices()
window.addEventListener('keydown', onKey)
})
onBeforeUnmount(() => {
clearTimeout(retry)
try { es?.close() } catch {}
window.removeEventListener('keydown', onKey)
})
const statusText = computed(() => ({
connecting: 'Connecting…',
starting: 'Loading speech model… (about 20 s the first time)',
ready: recording.value ? 'Listening' : 'Ready',
error: 'Engine error',
offline: 'Engine offline',
}[engine.value] || engine.value))
</script>
<template>
<div class="rudder" :class="{ rec: recording }">
<header class="top">
<div class="brand">
<span class="logo" aria-hidden="true">⎈</span>
<div>
<h1>Rudder</h1>
<p class="sub">Voice-to-text for vibe coders · private · runs on this computer · no internet</p>
</div>
</div>
<button class="info-btn" :aria-expanded="showInfo" title="What is Rudder?" aria-label="What is Rudder?" @click="showInfo = !showInfo">ⓘ</button>
<div class="status" :data-state="engine" role="status">
<span class="dot" aria-hidden="true"></span>{{ statusText }}
</div>
</header>
<section v-if="showInfo" class="info">
<h2>What is Rudder?</h2>
<p>A voice-to-text tool for vibe coders: talk to your agents instead of typing to them. Paste links, code
and notes into the box while you talk; everything you say is added to the end, ready to copy into a chat.</p>
<ul>
<li><b>Streaming.</b> Words appear while you speak, and you can keep talking for as long as you like —
an hour is fine. There is no time limit and nothing to wait for at the end.</li>
<li><b>Runs on your CPU.</b> No graphics card needed. The model uses about 1 GB of memory and keeps up with
normal speech on an ordinary laptop processor.</li>
<li><b>Private and offline.</b> Your voice never leaves this computer. No account, no cloud, no telemetry —
checked with a network tracer: zero connections while transcribing.</li>
<li><b>The model.</b> NVIDIA Nemotron 3.5 ASR Streaming (0.6B), run by NVIDIA's open-source
NeMo-Speech.cpp. It adds punctuation and capital letters by itself.</li>
<li><b>Why it lives in dx-maps.</b> Rudder is a plugin like the maps: built in the Plugin Studio, tested in the
browser, and shipped as a desktop app with its own local backend — proof that one dx-maps codebase can
build desktop apps, not only maps.</li>
</ul>
<p class="fine">Bluetooth headsets: Rudder switches them to their stable voice mode (CVSD), because the
higher-quality mode (mSBC) kept dropping the mic after a few words.</p>
</section>
<section class="controls">
<button
class="mic"
:class="{ on: recording }"
:disabled="busy || engine !== 'ready'"
:aria-pressed="recording"
:aria-label="recording ? 'Stop recording' : 'Start recording'"
@click="toggle"
>
<span class="mic-icon" aria-hidden="true">{{ recording ? '■' : '●' }}</span>
{{ recording ? 'Stop' : 'Record' }}
</button>
<div class="meter" aria-hidden="true"><div class="fill" :style="{ width: (level * 100).toFixed(0) + '%' }"></div></div>
<label class="lang">
<span>Language</span>
<select v-model="language" :disabled="recording" @change="save">
<option v-for="[code, name] in LANGUAGES" :key="code" :value="code">{{ name }}</option>
</select>
</label>
</section>
<section class="mics">
<label class="mic-pick">
<span>Microphone</span>
<select v-model="device" :disabled="recording || testing" @change="save(); testResult = null">
<option value="">System default{{ defaultDevice ? ' — ' + defaultDevice.label : '' }}</option>
<option v-for="d in devices" :key="d.id" :value="d.id">{{ deviceLabel(d) }}</option>
</select>
</label>
<button class="small" :disabled="testing || recording || engine === 'offline'" @click="testMic">
{{ testing ? 'Testing… speak now' : 'Test mic' }}
</button>
<button class="small ghost" title="Look for microphones again" aria-label="Look for microphones again" :disabled="recording" @click="refreshDevices">↻</button>
</section>
<p v-if="testResult" class="notice" :data-kind="testResult.ok ? (testResult.peak < 0.01 ? 'warn' : 'ok') : 'error'">
{{ testResult.ok ? '✓ ' : '✗ ' }}{{ testResult.verdict }}
</p>
<p v-if="chosen && chosen.muted" class="notice" data-kind="warn">“{{ chosen.label }}” is muted in your system sound settings — Rudder won't hear anything until it's unmuted.</p>
<p v-if="deviceError" class="notice" data-kind="error">{{ deviceError }}</p>
<p v-else-if="!devices.length && engine !== 'offline'" class="notice" data-kind="warn">No microphone found. Connect a headset or turn on your computer's microphone in Sound settings.</p>
<p v-if="offCards.length" class="hint">Also available but switched off in Sound settings: {{ offCards.join(', ') }}. Turn it on there to use it here.</p>
<p v-if="notice" class="notice" :data-kind="noticeKind">{{ notice }}</p>
<p v-else-if="engine === 'offline'" class="notice" data-kind="warn">
The local speech engine isn't reachable at <code>{{ apiBase }}</code>. In the Plugin Studio it starts with
<code>bun run dev</code> once built: <code>cd backend/rudder-engine &amp;&amp; go build -o ../bin/rudder-engine .</code>
</p>
<section class="compose">
<textarea
ref="box"
v-model="draft"
class="draft"
spellcheck="true"
aria-label="Your text. Type or paste here; everything Rudder hears is added to the end."
placeholder="Type or paste here — and press Record (Ctrl+Shift+Space) to talk. Your words stream into the end of this box as you speak."
@input="onUserInput"
@keydown="markTyping"
></textarea>
<p class="hearing" aria-live="polite">
<template v-if="recording"><span class="tag">listening</span> words appear at the end of the box as you speak — keep typing or pasting anywhere</template>
</p>
</section>
<footer class="bottom">
<button :disabled="!draft.trim()" @click="copy(draft, 'all')">{{ copied === 'all' ? 'Copied ✓' : 'Copy all' }}</button>
<button :disabled="!draft.trim() || recording" @click="clearAll">Clear</button>
<span class="count">{{ words }} {{ words === 1 ? 'word' : 'words' }}</span>
<span class="spacer"></span>
<button class="ghost" :aria-expanded="showDebug" @click="showDebug = !showDebug">🐞 Debug</button>
</footer>
<section v-if="showDebug" class="debug">
<dl>
<dt>API</dt><dd><code>{{ apiBase }}</code></dd>
<dt>Runtime</dt><dd>{{ runtime || '—' }}</dd>
<dt>Model</dt><dd>{{ model || '—' }}</dd>
<dt>Engine</dt><dd>{{ engine }}<span v-if="engineError"> — {{ engineError }}</span></dd>
</dl>
<button @click="copyDebug">{{ copied === 'debug' ? 'Copied ✓' : 'Copy debug report for an agent' }}</button>
</section>
</div>
</template>
<style>
:host { display: block; }
.rudder {
--bg: #0b0f14; --panel: #111823; --line: #1f2937; --text: #e5e7eb; --muted: #9ca3af;
--accent: #38bdf8; --rec: #ef4444; --ok: #22c55e; --warn: #f59e0b;
box-sizing: border-box; display: flex; flex-direction: column; gap: 14px; min-height: 480px; height: 100%;
padding: 18px; background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; font-size: 15px;
}
.rudder *, .rudder *::before, .rudder *::after { box-sizing: border-box; }
.top { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.brand { display: flex; align-items: center; gap: 12px; }
.logo { font-size: 30px; color: var(--accent); line-height: 1; }
h1 { margin: 0; font-size: 20px; letter-spacing: -0.01em; }
.sub { margin: 2px 0 0; color: var(--muted); font-size: 12.5px; }
.status { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--line);
border-radius: 999px; font-size: 13px; color: var(--muted); background: var(--panel); }
.status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
.status[data-state='ready'] .dot { background: var(--ok); }
.status[data-state='starting'] .dot, .status[data-state='connecting'] .dot { background: var(--warn); animation: pulse 1.2s infinite; }
.status[data-state='error'] .dot, .status[data-state='offline'] .dot { background: var(--rec); }
.rec .status .dot { background: var(--rec); animation: pulse 1s infinite; }
@keyframes pulse { 50% { opacity: .35; } }
.controls { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.mic { display: inline-flex; align-items: center; gap: 10px; padding: 12px 22px; border-radius: 999px;
border: 1px solid var(--line); background: var(--accent); color: #04121c; font-weight: 700; font-size: 16px; cursor: pointer; }
.mic.on { background: var(--rec); color: #fff; }
.mic:disabled { opacity: .45; cursor: not-allowed; }
.mic:focus-visible, button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.mic-icon { font-size: 14px; }
.meter { flex: 1 1 140px; min-width: 100px; height: 8px; border-radius: 4px; background: var(--panel); border: 1px solid var(--line); overflow: hidden; }
.meter .fill { height: 100%; background: linear-gradient(90deg, var(--ok), var(--warn), var(--rec)); transition: width .12s linear; }
.lang { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; }
select { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; font-size: 14px; }
.notice { margin: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel); font-size: 13.5px; line-height: 1.45; }
.notice[data-kind='error'] { border-color: #7f1d1d; background: #1f0f12; }
.notice[data-kind='warn'] { border-color: #78350f; background: #1c140a; }
.notice[data-kind='ok'] { border-color: #14532d; background: #0b1a11; }
.mics { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mic-pick { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; flex: 1 1 260px; }
.mic-pick select { flex: 1; min-width: 0; }
.small { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 7px 12px; cursor: pointer; font-size: 13.5px; }
.small:disabled { opacity: .45; cursor: not-allowed; }
.small.ghost { background: transparent; }
.hint { margin: 0; color: var(--muted); font-size: 12.5px; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; color: #c4b5fd; overflow-wrap: anywhere; }
.compose { flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; min-height: 260px; }
.draft { flex: 1 1 auto; width: 100%; min-height: 240px; resize: vertical; padding: 14px; border: 1px solid var(--line);
border-radius: 12px; background: var(--panel); color: var(--text); font: inherit; font-size: 16px; line-height: 1.6; }
.draft:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.draft::placeholder { color: #6b7280; }
.hearing { margin: 0; min-height: 1.5em; color: var(--accent); font-size: 14px; overflow-wrap: anywhere; }
.hearing .tag { display: inline-block; margin-right: 6px; padding: 1px 7px; border-radius: 999px; background: #0c2a3a;
color: var(--accent); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.count { color: var(--muted); font-size: 12.5px; }
.bottom { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.bottom button, .debug button { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px; }
.bottom button:disabled { opacity: .45; cursor: not-allowed; }
.bottom .ghost { background: transparent; }
.spacer { flex: 1; }
.info-btn { margin-left: auto; background: transparent; border: 1px solid var(--line); color: var(--muted); border-radius: 999px;
width: 30px; height: 30px; font-size: 16px; cursor: pointer; }
.info-btn:hover, .info-btn[aria-expanded='true'] { color: var(--accent); border-color: var(--accent); }
.info { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); padding: 14px 16px; font-size: 14px; line-height: 1.55; }
.info h2 { margin: 0 0 6px; font-size: 16px; }
.info p { margin: 0 0 8px; }
.info ul { margin: 0 0 8px; padding-left: 18px; }
.info li { margin-bottom: 6px; }
.info .fine { color: var(--muted); font-size: 12.5px; margin: 0; }
.debug { border: 1px dashed var(--line); border-radius: 10px; padding: 12px; font-size: 13px; }
.debug dl { display: grid; grid-template-columns: 90px 1fr; gap: 6px 10px; margin: 0 0 10px; }
.debug dt { color: var(--muted); }
.debug dd { margin: 0; overflow-wrap: anywhere; }
</style>

View file

@ -2,7 +2,7 @@
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
- **`dx-maps/`** — the output folder: 24 plugin bundles and 36 pages, self-contained.
- **`dx-maps/`** — the output folder: 25 plugin bundles and 37 pages, self-contained.
Copy this one folder to any web host (or open it from disk) and it works.
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
<script src="../all-people-groups.js" integrity="sha384-UpfV7r8lOCs9c6N6JEfQ/aVn0xv56b4ocMevjQ/+h2k7ehVSjnPhs8C2DLwkb4Xl" crossorigin="anonymous"></script>
<script src="../all-people-groups.js" integrity="sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -43,7 +43,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n" crossorigin="anonymous"></script>
<script src="../../big-picture.js" integrity="sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -43,7 +43,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n" crossorigin="anonymous"></script>
<script src="../../big-picture.js" integrity="sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -43,7 +43,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../big-picture.js" integrity="sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n" crossorigin="anonymous"></script>
<script src="../big-picture.js" integrity="sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-LdpIqKC96OsdMwLS0R8lJ17hr0siPfmIcpsM+WjSdTq8bPicSWNtcNeNNyfmx+44" crossorigin="anonymous"></script>
<script src="../../dashboard-plus-grid.js" integrity="sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-LdpIqKC96OsdMwLS0R8lJ17hr0siPfmIcpsM+WjSdTq8bPicSWNtcNeNNyfmx+44" crossorigin="anonymous"></script>
<script src="../dashboard-plus-grid.js" integrity="sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../demo-map.js" integrity="sha384-BzyptX/5BMjBESet3Qam1GKR6ho1cR76SymRCL2Z675awlg/mZPZFYQwDMRAMFxy" crossorigin="anonymous"></script>
<script src="../demo-map.js" integrity="sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -33,7 +33,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../../demo-map.js" integrity="sha384-BzyptX/5BMjBESet3Qam1GKR6ho1cR76SymRCL2Z675awlg/mZPZFYQwDMRAMFxy" crossorigin="anonymous"></script>
<script src="../../demo-map.js" integrity="sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../../doxa-maps-spa.js" integrity="sha384-SCoC5XH6cQd7bODSqrJtAEYtAGYIgp4FamEvfkNlip128htjOWs8uvSn4LD52ufG" crossorigin="anonymous"></script>
<script src="../../doxa-maps-spa.js" integrity="sha384-VIqwutIFWsxqEd/+MbGvhkTl8HFNyFaVODUb4e77KV6aVUTqpkjgT5SULi0srZo8" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../doxa-maps-spa.js" integrity="sha384-SCoC5XH6cQd7bODSqrJtAEYtAGYIgp4FamEvfkNlip128htjOWs8uvSn4LD52ufG" crossorigin="anonymous"></script>
<script src="../doxa-maps-spa.js" integrity="sha384-VIqwutIFWsxqEd/+MbGvhkTl8HFNyFaVODUb4e77KV6aVUTqpkjgT5SULi0srZo8" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -2,8 +2,8 @@
"schema": "doxa-maps-manifest@1",
"generatedFrom": "plugin-designs/*/*.vue",
"embedBase": ".",
"bundleCount": 24,
"profileCount": 36,
"bundleCount": 25,
"profileCount": 37,
"runtime": {
"apiBaseUrl": "https://pray.doxa.life"
},
@ -12,7 +12,7 @@
"bundle": "all-people-groups",
"scriptUrl": "./all-people-groups/all-people-groups.js",
"output": "dx-maps/all-people-groups/all-people-groups.js",
"integrity": "sha384-UpfV7r8lOCs9c6N6JEfQ/aVn0xv56b4ocMevjQ/+h2k7ehVSjnPhs8C2DLwkb4Xl",
"integrity": "sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx",
"elements": [
"all-people-groups"
],
@ -30,7 +30,7 @@
"scriptUrl": "./all-people-groups/all-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-UpfV7r8lOCs9c6N6JEfQ/aVn0xv56b4ocMevjQ/+h2k7ehVSjnPhs8C2DLwkb4Xl\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
},
"bundle": "all-people-groups"
}
@ -40,7 +40,7 @@
"bundle": "big-picture",
"scriptUrl": "./big-picture/big-picture.js",
"output": "dx-maps/big-picture/big-picture.js",
"integrity": "sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n",
"integrity": "sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP",
"elements": [
"bp-big-picture",
"bp-research-header",
@ -64,7 +64,7 @@
"scriptUrl": "./big-picture/big-picture.js",
"embed": {
"iframe": "<iframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
},
"instances": [
{
@ -108,7 +108,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "dx-maps/cities-internet/cities-internet.js",
"integrity": "sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6",
"integrity": "sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC",
"elements": [
"cities-internet-map"
],
@ -126,7 +126,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -141,7 +141,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -156,7 +156,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
}
]
@ -165,7 +165,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr",
"integrity": "sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd",
"elements": [
"country-posters-map"
],
@ -187,7 +187,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -202,7 +202,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -217,7 +217,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -226,7 +226,7 @@
"bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-LdpIqKC96OsdMwLS0R8lJ17hr0siPfmIcpsM+WjSdTq8bPicSWNtcNeNNyfmx+44",
"integrity": "sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw",
"elements": [
"dashboard-plus-grid"
],
@ -248,7 +248,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-LdpIqKC96OsdMwLS0R8lJ17hr0siPfmIcpsM+WjSdTq8bPicSWNtcNeNNyfmx+44\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
},
"instances": [
{
@ -272,7 +272,7 @@
"bundle": "demo-map",
"scriptUrl": "./demo-map/demo-map.js",
"output": "dx-maps/demo-map/demo-map.js",
"integrity": "sha384-BzyptX/5BMjBESet3Qam1GKR6ho1cR76SymRCL2Z675awlg/mZPZFYQwDMRAMFxy",
"integrity": "sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6",
"elements": [
"demo-map"
],
@ -290,7 +290,7 @@
"scriptUrl": "./demo-map/demo-map.js",
"embed": {
"iframe": "<iframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-BzyptX/5BMjBESet3Qam1GKR6ho1cR76SymRCL2Z675awlg/mZPZFYQwDMRAMFxy\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
},
"instances": [
{
@ -317,7 +317,7 @@
"bundle": "doxa-maps-spa",
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
"integrity": "sha384-SCoC5XH6cQd7bODSqrJtAEYtAGYIgp4FamEvfkNlip128htjOWs8uvSn4LD52ufG",
"integrity": "sha384-VIqwutIFWsxqEd/+MbGvhkTl8HFNyFaVODUb4e77KV6aVUTqpkjgT5SULi0srZo8",
"elements": [
"doxa-maps-spa"
],
@ -344,7 +344,7 @@
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"embed": {
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-SCoC5XH6cQd7bODSqrJtAEYtAGYIgp4FamEvfkNlip128htjOWs8uvSn4LD52ufG\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-VIqwutIFWsxqEd/+MbGvhkTl8HFNyFaVODUb4e77KV6aVUTqpkjgT5SULi0srZo8\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
},
"instances": [
{
@ -371,7 +371,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-XRdaz1XJcFu2UK8MaZI54lEEqOi/kyoy7rPmGGMxbeNu6BfTyvK6s27oT/3w1N68",
"integrity": "sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7",
"elements": [
"doxa-research-map"
],
@ -389,7 +389,7 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-XRdaz1XJcFu2UK8MaZI54lEEqOi/kyoy7rPmGGMxbeNu6BfTyvK6s27oT/3w1N68\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
},
"instances": [
{
@ -415,7 +415,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O",
"integrity": "sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -434,7 +434,7 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
},
"instances": [
{
@ -604,7 +604,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3",
"integrity": "sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA",
"elements": [
"gospel-internet-board"
],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{
@ -723,7 +723,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "dx-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU",
"integrity": "sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq",
"elements": [
"gospel-resources-map"
],
@ -745,7 +745,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -760,7 +760,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -775,7 +775,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -790,7 +790,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -805,7 +805,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]
@ -814,7 +814,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw",
"integrity": "sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR",
"elements": [
"my-upg-100-list-map"
],
@ -836,7 +836,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -851,7 +851,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -866,7 +866,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -875,7 +875,7 @@
"bundle": "people-groups-grid",
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
"output": "dx-maps/people-groups-grid/people-groups-grid.js",
"integrity": "sha384-MOXIP3z6CfUulsBpfJTVhvT/jkXcC6HbwmMM7JNf1NZ6/kH9KTM7tejKa0yzMYez",
"integrity": "sha384-iSSXDdIeHrAuuvA8zvtD2unbSq7DozG9x6wTncSFUzfdHGgRuiO2VzM7NM3qCVsO",
"elements": [
"people-groups-grid"
],
@ -893,7 +893,7 @@
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-MOXIP3z6CfUulsBpfJTVhvT/jkXcC6HbwmMM7JNf1NZ6/kH9KTM7tejKa0yzMYez\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-iSSXDdIeHrAuuvA8zvtD2unbSq7DozG9x6wTncSFUzfdHGgRuiO2VzM7NM3qCVsO\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
},
"instances": [
{
@ -938,7 +938,7 @@
"bundle": "plugin-studio",
"scriptUrl": "./plugin-studio/plugin-studio.js",
"output": "dx-maps/plugin-studio/plugin-studio.js",
"integrity": "sha384-FGKp8p589aDTbWv3yPS40qCABh4XNwpgRHkBH09+5OjsnIPboIrQxnlf+owt6C2O",
"integrity": "sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX",
"elements": [
"plugin-studio"
],
@ -968,7 +968,7 @@
"scriptUrl": "./plugin-studio/plugin-studio.js",
"embed": {
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-FGKp8p589aDTbWv3yPS40qCABh4XNwpgRHkBH09+5OjsnIPboIrQxnlf+owt6C2O\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
},
"instances": [
{
@ -995,7 +995,7 @@
"bundle": "pplr-data-maps",
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
"output": "dx-maps/pplr-data-maps/pplr-data-maps.js",
"integrity": "sha384-MP9V4WJZS2QENIY0/jG9lV8ulokC2vTP4rfK447aXPDtwc/Rucjv0lUpNdMoX5e/",
"integrity": "sha384-8qvVelBu65X8Z2M0SHe/Bk+aXULbXdJXUqf/pAwan3DBsXnCBNp2Agl3JB+V0WWT",
"elements": [
"pplr-data-maps"
],
@ -1013,7 +1013,7 @@
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
"embed": {
"iframe": "<iframe src=\"./pplr-data-maps/pplr-data-maps/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-MP9V4WJZS2QENIY0/jG9lV8ulokC2vTP4rfK447aXPDtwc/Rucjv0lUpNdMoX5e/\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-8qvVelBu65X8Z2M0SHe/Bk+aXULbXdJXUqf/pAwan3DBsXnCBNp2Agl3JB+V0WWT\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
},
"bundle": "pplr-data-maps"
}
@ -1050,7 +1050,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h",
"integrity": "sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q",
"elements": [
"regions-dashboard"
],
@ -1072,7 +1072,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1087,7 +1087,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1102,7 +1102,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]
@ -1111,7 +1111,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "dx-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-NXO+WIp3FoLVpHj7LVHb6YEm05dX1Ur/8g4JGbRZ3boDOJy1hnSj6nzvdX50uBO7",
"integrity": "sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB",
"elements": [
"research-dashboard-map"
],
@ -1129,7 +1129,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-NXO+WIp3FoLVpHj7LVHb6YEm05dX1Ur/8g4JGbRZ3boDOJy1hnSj6nzvdX50uBO7\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}
@ -1181,6 +1181,33 @@
}
]
},
{
"bundle": "rudder-voice",
"scriptUrl": "./rudder-voice/rudder-voice.js",
"output": "dx-maps/rudder-voice/rudder-voice.js",
"integrity": "sha384-ygcWKoVAImcBol0tmjxbdFMmgZpzITjrPt97BqNUPVXacNvhhoeiCgqiPHbSJVC9",
"elements": [
"rudder-voice"
],
"stage": "experimental",
"profiles": [
{
"profile": "rudder-voice",
"stage": "experimental",
"file": "rudder-voice.vue",
"description": "A DESKTOP-target plugin (rudder-voice.meta.json \"targets\": [\"desktop\"]). The speech",
"element": "rudder-voice",
"type": "single",
"isMap": false,
"iframeUrl": "./rudder-voice/rudder-voice/index.html",
"scriptUrl": "./rudder-voice/rudder-voice.js",
"embed": {
"iframe": "<iframe src=\"./rudder-voice/rudder-voice/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./rudder-voice/rudder-voice.js\" integrity=\"sha384-ygcWKoVAImcBol0tmjxbdFMmgZpzITjrPt97BqNUPVXacNvhhoeiCgqiPHbSJVC9\" crossorigin=\"anonymous\"></script>\n<rudder-voice profile-config='{\"profile\":\"rudder-voice\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></rudder-voice>"
}
}
]
},
{
"bundle": "stack-demo",
"scriptUrl": "./stack-demo/stack-demo.js",
@ -1231,7 +1258,7 @@
"bundle": "upg-neighbor",
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"output": "dx-maps/upg-neighbor/upg-neighbor.js",
"integrity": "sha384-mQcftR3NMkr1fnmdwOglvyTnxUm1l9ThUMBxfOjRuA+DByp/zfMlmYo5a/l8kaSL",
"integrity": "sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+",
"elements": [
"upg-neighbor"
],
@ -1249,7 +1276,7 @@
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"embed": {
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-mQcftR3NMkr1fnmdwOglvyTnxUm1l9ThUMBxfOjRuA+DByp/zfMlmYo5a/l8kaSL\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
},
"instances": [
{
@ -1331,7 +1358,7 @@
"bundle": "wall-of-people-groups",
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
"integrity": "sha384-5wAG2q1aIqWtVJYfxzMQLsXwZckC1xTtkQD3xQgNgobmZ4+cCGwqqvSC8AAXpPOJ",
"integrity": "sha384-iOK7UmX7y0NpxOmDDBHjupJIv8ozWx+zhVqw3rWhUid0htACzXsF78F4VNPt3uAZ",
"elements": [
"wall-of-people-groups"
],
@ -1349,7 +1376,7 @@
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-5wAG2q1aIqWtVJYfxzMQLsXwZckC1xTtkQD3xQgNgobmZ4+cCGwqqvSC8AAXpPOJ\" crossorigin=\"anonymous\"></script>\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-iOK7UmX7y0NpxOmDDBHjupJIv8ozWx+zhVqw3rWhUid0htACzXsF78F4VNPt3uAZ\" crossorigin=\"anonymous\"></script>\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
},
"bundle": "wall-of-people-groups"
}

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-XRdaz1XJcFu2UK8MaZI54lEEqOi/kyoy7rPmGGMxbeNu6BfTyvK6s27oT/3w1N68" crossorigin="anonymous"></script>
<script src="../doxa-research-map.js" integrity="sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -33,7 +33,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../../doxa-research-map.js" integrity="sha384-XRdaz1XJcFu2UK8MaZI54lEEqOi/kyoy7rPmGGMxbeNu6BfTyvK6s27oT/3w1N68" crossorigin="anonymous"></script>
<script src="../../doxa-research-map.js" integrity="sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../doxa-simple-map.js" integrity="sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O" crossorigin="anonymous"></script>
<script src="../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -19,6 +19,6 @@
</head>
<body>
<plugin-studio tree-base="."></plugin-studio>
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-FGKp8p589aDTbWv3yPS40qCABh4XNwpgRHkBH09+5OjsnIPboIrQxnlf+owt6C2O" crossorigin="anonymous"></script>
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX" crossorigin="anonymous"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -2,8 +2,8 @@
"schema": "doxa-maps-manifest@1",
"generatedFrom": "plugin-designs/*/*.vue",
"embedBase": ".",
"bundleCount": 24,
"profileCount": 36,
"bundleCount": 25,
"profileCount": 37,
"runtime": {
"apiBaseUrl": "https://pray.doxa.life"
},
@ -12,7 +12,7 @@
"bundle": "all-people-groups",
"scriptUrl": "./all-people-groups/all-people-groups.js",
"output": "dx-maps/all-people-groups/all-people-groups.js",
"integrity": "sha384-UpfV7r8lOCs9c6N6JEfQ/aVn0xv56b4ocMevjQ/+h2k7ehVSjnPhs8C2DLwkb4Xl",
"integrity": "sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx",
"elements": [
"all-people-groups"
],
@ -30,7 +30,7 @@
"scriptUrl": "./all-people-groups/all-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-UpfV7r8lOCs9c6N6JEfQ/aVn0xv56b4ocMevjQ/+h2k7ehVSjnPhs8C2DLwkb4Xl\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-xpQXT3X2W81IPWaSXifrP9kXGf/nbU8hwT5ZWMQjH0EwDWHMJeAiWj+yEuAi+KBx\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
},
"bundle": "all-people-groups"
}
@ -40,7 +40,7 @@
"bundle": "big-picture",
"scriptUrl": "./big-picture/big-picture.js",
"output": "dx-maps/big-picture/big-picture.js",
"integrity": "sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n",
"integrity": "sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP",
"elements": [
"bp-big-picture",
"bp-research-header",
@ -64,7 +64,7 @@
"scriptUrl": "./big-picture/big-picture.js",
"embed": {
"iframe": "<iframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-VoI56FhJLieze4gUEG6DPjj8mnd8HLLPeGqbr9rw65+nxJ0S+cLZtH24dpTENg8n\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-vq5/y8EkBMQyCQFvBmoWBZhBX73c4vln8KD5tuMyMdhzMDf25zyUTdUx4XRAEibP\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
},
"instances": [
{
@ -108,7 +108,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "dx-maps/cities-internet/cities-internet.js",
"integrity": "sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6",
"integrity": "sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC",
"elements": [
"cities-internet-map"
],
@ -126,7 +126,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -141,7 +141,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -156,7 +156,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-PZG4YBoVja7c6wLx2MVam5YVFy9h3SGF59LgSf2PeE5eT7z2yMDcmFs4iW3ZHej6\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-I2iNfp1pTAhCbpzIz/iVdUo0/BgsDr7xskuZtOfYw8mP+2e6MnmRKH8erYGOhJsC\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
}
]
@ -165,7 +165,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr",
"integrity": "sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd",
"elements": [
"country-posters-map"
],
@ -187,7 +187,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -202,7 +202,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -217,7 +217,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-ED4gn7N1cdIR5W91wnxyaBaQbrt83Ay2WfsqL0LhoRaWfEf6KhKsu+zP7mH1b8nr\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-gWxzvc16MGYwzrZ6Z/XBGaPkI5Ezh45OMepMoAXMj/USGmin8hgMCR7bVhVa5Bcd\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -226,7 +226,7 @@
"bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-LdpIqKC96OsdMwLS0R8lJ17hr0siPfmIcpsM+WjSdTq8bPicSWNtcNeNNyfmx+44",
"integrity": "sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw",
"elements": [
"dashboard-plus-grid"
],
@ -248,7 +248,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-LdpIqKC96OsdMwLS0R8lJ17hr0siPfmIcpsM+WjSdTq8bPicSWNtcNeNNyfmx+44\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-gLs4MQRznMyg9cKyDneIKEkjCDfd/Buvv+G8Xwwn+ggxaKshHtD1zKO658e8L6pw\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
},
"instances": [
{
@ -272,7 +272,7 @@
"bundle": "demo-map",
"scriptUrl": "./demo-map/demo-map.js",
"output": "dx-maps/demo-map/demo-map.js",
"integrity": "sha384-BzyptX/5BMjBESet3Qam1GKR6ho1cR76SymRCL2Z675awlg/mZPZFYQwDMRAMFxy",
"integrity": "sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6",
"elements": [
"demo-map"
],
@ -290,7 +290,7 @@
"scriptUrl": "./demo-map/demo-map.js",
"embed": {
"iframe": "<iframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-BzyptX/5BMjBESet3Qam1GKR6ho1cR76SymRCL2Z675awlg/mZPZFYQwDMRAMFxy\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-IVZC654ptY88sI56Xw0Mlmc58TNBXDzTR2+WrGRwv5LYPALSneviSCNrpg61Wzi6\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
},
"instances": [
{
@ -317,7 +317,7 @@
"bundle": "doxa-maps-spa",
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
"integrity": "sha384-SCoC5XH6cQd7bODSqrJtAEYtAGYIgp4FamEvfkNlip128htjOWs8uvSn4LD52ufG",
"integrity": "sha384-VIqwutIFWsxqEd/+MbGvhkTl8HFNyFaVODUb4e77KV6aVUTqpkjgT5SULi0srZo8",
"elements": [
"doxa-maps-spa"
],
@ -344,7 +344,7 @@
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"embed": {
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-SCoC5XH6cQd7bODSqrJtAEYtAGYIgp4FamEvfkNlip128htjOWs8uvSn4LD52ufG\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-VIqwutIFWsxqEd/+MbGvhkTl8HFNyFaVODUb4e77KV6aVUTqpkjgT5SULi0srZo8\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
},
"instances": [
{
@ -371,7 +371,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-XRdaz1XJcFu2UK8MaZI54lEEqOi/kyoy7rPmGGMxbeNu6BfTyvK6s27oT/3w1N68",
"integrity": "sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7",
"elements": [
"doxa-research-map"
],
@ -389,7 +389,7 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-XRdaz1XJcFu2UK8MaZI54lEEqOi/kyoy7rPmGGMxbeNu6BfTyvK6s27oT/3w1N68\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-QF6F4GttfIpkev5vLjQn5SftilFmw7HksE+bvoYu5WccjETMjVxPoeqEB3wqmGh7\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
},
"instances": [
{
@ -415,7 +415,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O",
"integrity": "sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -434,7 +434,7 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-NTCkI6p+Ehv5y1Y0eyABnBMdJumMkTyiCWqwUEH14SQvz2sQt8rZPKgzIv0v/Y4O\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-qz7rw+kKZQs04E7Am98uyFYO8TfhD8uGy73NUFfbiR7Nag2rON9XEyjzgeg6c6Yn\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
},
"instances": [
{
@ -604,7 +604,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3",
"integrity": "sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA",
"elements": [
"gospel-internet-board"
],
@ -623,7 +623,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-AFzliHG0FcJCyvXJTFd/Rseclmdq3j7CuWGEse/qjcYDKdSw5fJxad/EII+71Ac3\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-WlhqCwOlpEcSZHEs5InjwwTpU7ZWYGX6MvdRCuwnRpBzNYo8El4OBUSVvm2ZmARA\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{
@ -723,7 +723,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "dx-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU",
"integrity": "sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq",
"elements": [
"gospel-resources-map"
],
@ -745,7 +745,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -760,7 +760,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -775,7 +775,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -790,7 +790,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -805,7 +805,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-le8HFTops/Qex/thcP8lqvflRNpVv38hsQNuLQg4BNBkmNeY5JfwElBAF61FD+SU\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-heRNvaCP4oO4GsoQg8gWD5/mXBkUm6d1CEVUqyzJ/Jh3Mr50waoiOEp4RCn6CfEq\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]
@ -814,7 +814,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw",
"integrity": "sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR",
"elements": [
"my-upg-100-list-map"
],
@ -836,7 +836,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -851,7 +851,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -866,7 +866,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -875,7 +875,7 @@
"bundle": "people-groups-grid",
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
"output": "dx-maps/people-groups-grid/people-groups-grid.js",
"integrity": "sha384-MOXIP3z6CfUulsBpfJTVhvT/jkXcC6HbwmMM7JNf1NZ6/kH9KTM7tejKa0yzMYez",
"integrity": "sha384-iSSXDdIeHrAuuvA8zvtD2unbSq7DozG9x6wTncSFUzfdHGgRuiO2VzM7NM3qCVsO",
"elements": [
"people-groups-grid"
],
@ -893,7 +893,7 @@
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-MOXIP3z6CfUulsBpfJTVhvT/jkXcC6HbwmMM7JNf1NZ6/kH9KTM7tejKa0yzMYez\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-iSSXDdIeHrAuuvA8zvtD2unbSq7DozG9x6wTncSFUzfdHGgRuiO2VzM7NM3qCVsO\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
},
"instances": [
{
@ -938,7 +938,7 @@
"bundle": "plugin-studio",
"scriptUrl": "./plugin-studio/plugin-studio.js",
"output": "dx-maps/plugin-studio/plugin-studio.js",
"integrity": "sha384-FGKp8p589aDTbWv3yPS40qCABh4XNwpgRHkBH09+5OjsnIPboIrQxnlf+owt6C2O",
"integrity": "sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX",
"elements": [
"plugin-studio"
],
@ -968,7 +968,7 @@
"scriptUrl": "./plugin-studio/plugin-studio.js",
"embed": {
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-FGKp8p589aDTbWv3yPS40qCABh4XNwpgRHkBH09+5OjsnIPboIrQxnlf+owt6C2O\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-IQjG7XqoeppiQ+mxxtUxQApF7LoNWEc+LuQEftMZxWdDyht1X1u0mnB6LvDKAMJX\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
},
"instances": [
{
@ -995,7 +995,7 @@
"bundle": "pplr-data-maps",
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
"output": "dx-maps/pplr-data-maps/pplr-data-maps.js",
"integrity": "sha384-MP9V4WJZS2QENIY0/jG9lV8ulokC2vTP4rfK447aXPDtwc/Rucjv0lUpNdMoX5e/",
"integrity": "sha384-8qvVelBu65X8Z2M0SHe/Bk+aXULbXdJXUqf/pAwan3DBsXnCBNp2Agl3JB+V0WWT",
"elements": [
"pplr-data-maps"
],
@ -1013,7 +1013,7 @@
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
"embed": {
"iframe": "<iframe src=\"./pplr-data-maps/pplr-data-maps/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-MP9V4WJZS2QENIY0/jG9lV8ulokC2vTP4rfK447aXPDtwc/Rucjv0lUpNdMoX5e/\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-8qvVelBu65X8Z2M0SHe/Bk+aXULbXdJXUqf/pAwan3DBsXnCBNp2Agl3JB+V0WWT\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
},
"bundle": "pplr-data-maps"
}
@ -1050,7 +1050,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h",
"integrity": "sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q",
"elements": [
"regions-dashboard"
],
@ -1072,7 +1072,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1087,7 +1087,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1102,7 +1102,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-sgxkZZPgyYWkzDU6pgc5iLfW6/VKxbXskhXCBi1VDl6Zf6FWDTejuqgN07yzpI3h\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Ah45bl/Z2Qirx42bUMN9sh9/B/OlA2HahdujWOX8lIOONPT868Q2ct/ehMJ/I41Q\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]
@ -1111,7 +1111,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "dx-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-NXO+WIp3FoLVpHj7LVHb6YEm05dX1Ur/8g4JGbRZ3boDOJy1hnSj6nzvdX50uBO7",
"integrity": "sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB",
"elements": [
"research-dashboard-map"
],
@ -1129,7 +1129,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-NXO+WIp3FoLVpHj7LVHb6YEm05dX1Ur/8g4JGbRZ3boDOJy1hnSj6nzvdX50uBO7\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-GBlVmWSyzwQX0ty93zufhUFNTSJoDn8RF+7ywLJ0Q5sx5RLUoC2yt7W/XPtuX7LB\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}
@ -1181,6 +1181,33 @@
}
]
},
{
"bundle": "rudder-voice",
"scriptUrl": "./rudder-voice/rudder-voice.js",
"output": "dx-maps/rudder-voice/rudder-voice.js",
"integrity": "sha384-ygcWKoVAImcBol0tmjxbdFMmgZpzITjrPt97BqNUPVXacNvhhoeiCgqiPHbSJVC9",
"elements": [
"rudder-voice"
],
"stage": "experimental",
"profiles": [
{
"profile": "rudder-voice",
"stage": "experimental",
"file": "rudder-voice.vue",
"description": "A DESKTOP-target plugin (rudder-voice.meta.json \"targets\": [\"desktop\"]). The speech",
"element": "rudder-voice",
"type": "single",
"isMap": false,
"iframeUrl": "./rudder-voice/rudder-voice/index.html",
"scriptUrl": "./rudder-voice/rudder-voice.js",
"embed": {
"iframe": "<iframe src=\"./rudder-voice/rudder-voice/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./rudder-voice/rudder-voice.js\" integrity=\"sha384-ygcWKoVAImcBol0tmjxbdFMmgZpzITjrPt97BqNUPVXacNvhhoeiCgqiPHbSJVC9\" crossorigin=\"anonymous\"></script>\n<rudder-voice profile-config='{\"profile\":\"rudder-voice\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></rudder-voice>"
}
}
]
},
{
"bundle": "stack-demo",
"scriptUrl": "./stack-demo/stack-demo.js",
@ -1231,7 +1258,7 @@
"bundle": "upg-neighbor",
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"output": "dx-maps/upg-neighbor/upg-neighbor.js",
"integrity": "sha384-mQcftR3NMkr1fnmdwOglvyTnxUm1l9ThUMBxfOjRuA+DByp/zfMlmYo5a/l8kaSL",
"integrity": "sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+",
"elements": [
"upg-neighbor"
],
@ -1249,7 +1276,7 @@
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"embed": {
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-mQcftR3NMkr1fnmdwOglvyTnxUm1l9ThUMBxfOjRuA+DByp/zfMlmYo5a/l8kaSL\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-D2e4OB60VjRAhfnyVG192dx3hxC9tfP1WQMu1E+JcINQ2XRO7tCUhXXNZBMoQrD+\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
},
"instances": [
{
@ -1331,7 +1358,7 @@
"bundle": "wall-of-people-groups",
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
"integrity": "sha384-5wAG2q1aIqWtVJYfxzMQLsXwZckC1xTtkQD3xQgNgobmZ4+cCGwqqvSC8AAXpPOJ",
"integrity": "sha384-iOK7UmX7y0NpxOmDDBHjupJIv8ozWx+zhVqw3rWhUid0htACzXsF78F4VNPt3uAZ",
"elements": [
"wall-of-people-groups"
],
@ -1349,7 +1376,7 @@
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-5wAG2q1aIqWtVJYfxzMQLsXwZckC1xTtkQD3xQgNgobmZ4+cCGwqqvSC8AAXpPOJ\" crossorigin=\"anonymous\"></script>\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-iOK7UmX7y0NpxOmDDBHjupJIv8ozWx+zhVqw3rWhUid0htACzXsF78F4VNPt3uAZ\" crossorigin=\"anonymous\"></script>\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
},
"bundle": "wall-of-people-groups"
}

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-Cnp2SXHjT1n9iXjyvkRdWGiPojhufIfdHtKHhc0hTWg4LB8SFbwSgcoHQOaVt3Yw" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-zw1aZwvqZImE3ACuMOXExalaEsDUF+rKTxA+AQWZYnlTlW71kawA8QI6YrOIOGmR" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more