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:
commit
7332ffc377
127 changed files with 4284 additions and 957 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
456
backend/rudder-engine/devices.go
Normal file
456
backend/rudder-engine/devices.go
Normal 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()
|
||||
}
|
||||
5
backend/rudder-engine/go.mod
Normal file
5
backend/rudder-engine/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module dx-maps/backend/rudder-engine
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require github.com/gorilla/websocket v1.5.3
|
||||
2
backend/rudder-engine/go.sum
Normal file
2
backend/rudder-engine/go.sum
Normal 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=
|
||||
797
backend/rudder-engine/main.go
Normal file
797
backend/rudder-engine/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
59
backend/rudder-engine/setup-runtime.sh
Executable file
59
backend/rudder-engine/setup-runtime.sh
Executable 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."
|
||||
23
data/people-groups/christian-share.json
Normal file
23
data/people-groups/christian-share.json
Normal 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
|
||||
}
|
||||
}
|
||||
21
deployment/cloud/Dockerfile
Normal file
21
deployment/cloud/Dockerfile
Normal 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"]
|
||||
45
deployment/cloud/README.md
Normal file
45
deployment/cloud/README.md
Normal 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
65
deployment/cloud/entry.sh
Executable 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 $?
|
||||
22
deployment/cloud/vite.cloud.config.mjs
Normal file
22
deployment/cloud/vite.cloud.config.mjs
Normal 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 } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
61
internal/backend-dev-plugin.mjs
Normal file
61
internal/backend-dev-plugin.mjs
Normal 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) })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
},
|
||||
"upg100WallHeading": "قائمتي UPG 100 — قوائم الصلاة والملصقات",
|
||||
"upg100WallTag": "تعبئة للصلاة",
|
||||
"upg100WallIntro": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم <strong>باسم شعبهم</strong>.",
|
||||
"upg100WallIntroA": "وزّع مهمة الصلاة في مجموعات من خمسين ومئة — كما أطعم يسوع الخمسة آلاف (لوقا ٩:١٤). كل ملصق هو قائمة من 100 يمكنك أن تصلي بها وتطبعها وتعلّقها على الحائط — مصلّيًا لأجلهم ",
|
||||
"upg100WallIntroEmph": "باسم شعبهم",
|
||||
"upg100WallIntroB": ".",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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": ".",
|
||||
|
|
|
|||
|
|
@ -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": ".",
|
||||
|
|
|
|||
|
|
@ -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": ".",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
},
|
||||
"upg100WallHeading": "मेरी UPG 100 सूची — प्रार्थना सूचियाँ और पोस्टर",
|
||||
"upg100WallTag": "प्रार्थना लामबंदी",
|
||||
"upg100WallIntro": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए <strong>उनके जनसमूह के नाम से</strong> प्रार्थना करते हुए।",
|
||||
"upg100WallIntroA": "प्रार्थना के काम को पचास-पचास और सौ-सौ के समूहों में बाँटें — जैसे यीशु ने 5,000 लोगों को खिलाया (लूका 9:14)। हर पोस्टर 100 की एक सूची है जिसके लिए आप प्रार्थना कर सकते हैं, छाप सकते हैं और दीवार पर लगा सकते हैं — उनके लिए ",
|
||||
"upg100WallIntroEmph": "उनके जनसमूह के नाम से",
|
||||
"upg100WallIntroB": " प्रार्थना करते हुए।",
|
||||
|
|
|
|||
|
|
@ -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": ".",
|
||||
|
|
|
|||
|
|
@ -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": ".",
|
||||
|
|
|
|||
|
|
@ -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": ".",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
},
|
||||
"upg100WallHeading": "Мой список UPG 100 — молитвенные списки и плакаты",
|
||||
"upg100WallTag": "Молитвенная мобилизация",
|
||||
"upg100WallIntro": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них <strong>по имени их народа</strong>.",
|
||||
"upg100WallIntroA": "Разделите молитвенный труд на группы по пятьдесят и по сто — так Иисус накормил 5000 (Луки 9:14). Каждый плакат — это список из 100, о котором можно молиться, распечатать и повесить на стену, молясь о них ",
|
||||
"upg100WallIntroEmph": "по имени их народа",
|
||||
"upg100WallIntroB": ".",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
},
|
||||
"upg100WallHeading": "我的 UPG 100 名單 — 禱告名單與海報",
|
||||
"upg100WallTag": "禱告動員",
|
||||
"upg100WallIntro": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — <strong>按著他們族群的名字</strong>為他們禱告。",
|
||||
"upg100WallIntroA": "將禱告的工作分成五十人、一百人一組 — 就像耶穌餵飽五千人那樣(路加福音 9:14)。每一張海報都是一份 100 人名單,你可以為他們禱告、列印出來、貼在牆上 — ",
|
||||
"upg100WallIntroEmph": "按著他們族群的名字",
|
||||
"upg100WallIntroB": "為他們禱告。",
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "当做什么"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
55
plugin-designs/rudder-voice/README.md
Normal file
55
plugin-designs/rudder-voice/README.md
Normal 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.
|
||||
32
plugin-designs/rudder-voice/index.html
Normal file
32
plugin-designs/rudder-voice/index.html
Normal 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>
|
||||
23
plugin-designs/rudder-voice/index.js
Normal file
23
plugin-designs/rudder-voice/index.js
Normal 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
|
||||
7
plugin-designs/rudder-voice/rudder-voice.meta.json
Normal file
7
plugin-designs/rudder-voice/rudder-voice.meta.json
Normal 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"
|
||||
}
|
||||
650
plugin-designs/rudder-voice/rudder-voice.vue
Normal file
650
plugin-designs/rudder-voice/rudder-voice.vue
Normal 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 && 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>
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue