diff --git a/app/demo/document.go b/app/demo/document.go
new file mode 100644
index 0000000..9d0b97b
--- /dev/null
+++ b/app/demo/document.go
@@ -0,0 +1,5 @@
+package main
+
+import "syscall/js"
+
+var document = js.Global().Get("document")
diff --git a/app/demo/index.html b/app/demo/index.html
new file mode 100644
index 0000000..ca18c54
--- /dev/null
+++ b/app/demo/index.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/demo/log.go b/app/demo/log.go
new file mode 100644
index 0000000..cf3b9fe
--- /dev/null
+++ b/app/demo/log.go
@@ -0,0 +1,42 @@
+package main
+
+import "syscall/js"
+
+type LogWriter struct {
+ Element js.Value
+ Container js.Value
+}
+
+func (pw *LogWriter) ScrollToBottom() {
+ scrollHeight := pw.Container.Get("scrollHeight").Int()
+ clientHeight := pw.Container.Get("clientHeight").Int()
+ pw.Container.Set("scrollTop", scrollHeight-clientHeight)
+}
+
+func (pw *LogWriter) IsScrolledToBottom() bool {
+ scrollHeight := pw.Container.Get("scrollHeight").Int()
+ clientHeight := pw.Container.Get("clientHeight").Int()
+ scrollTop := pw.Container.Get("scrollTop").Int()
+ return scrollHeight-clientHeight <= scrollTop+25
+}
+
+func (pw *LogWriter) Write(p []byte) (n int, err error) {
+ if !pw.Element.IsUndefined() {
+ currentText := pw.Element.Get("textContent").String()
+ newText := currentText + string(p)
+ pw.Element.Set("textContent", newText)
+ if pw.IsScrolledToBottom() {
+ pw.ScrollToBottom()
+ }
+ }
+ return len(p), nil
+}
+
+func NewLogWriter(id string) *LogWriter {
+ element := document.Call("getElementById", id)
+ container := element.Get("parentElement")
+ return &LogWriter{
+ Element: element,
+ Container: container,
+ }
+}
diff --git a/app/demo/main.go b/app/demo/main.go
new file mode 100644
index 0000000..929abf7
--- /dev/null
+++ b/app/demo/main.go
@@ -0,0 +1,121 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+
+ "github.com/jwetzell/showbridge-go"
+ "github.com/jwetzell/showbridge-go/internal/config"
+)
+
+func main() {
+
+ slog.SetLogLoggerLevel(slog.LevelDebug)
+ slog.SetDefault(slog.New(slog.NewTextHandler(NewLogWriter("logs"), &slog.HandlerOptions{
+ Level: slog.LevelDebug,
+ })))
+
+ router, moduleConfigErrors, routeConfigErrors := showbridge.NewRouter(config.Config{
+ Api: config.ApiConfig{
+ Enabled: false,
+ Port: 0,
+ },
+ Modules: []config.ModuleConfig{
+ {
+ Id: "timer",
+ Type: "time.interval",
+ Params: map[string]any{
+ "duration": 1000,
+ },
+ },
+ {
+ Id: "button1",
+ Type: "web.onclick",
+ Params: map[string]any{
+ "id": "button1",
+ },
+ },
+ {
+ Id: "button2",
+ Type: "web.onclick",
+ Params: map[string]any{
+ "id": "button2",
+ },
+ },
+ },
+ Routes: []config.RouteConfig{
+ {
+ Input: "timer",
+ Processors: []config.ProcessorConfig{
+ {
+ Type: "debug.log",
+ },
+ },
+ },
+ {
+ Input: "button1",
+ Processors: []config.ProcessorConfig{
+ {
+ Type: "string.create",
+ Params: map[string]any{
+ "template": "{{.Payload.UnixMilli}}",
+ },
+ },
+ {
+ Type: "debug.log",
+ },
+ {
+ Type: "web.set",
+ Params: map[string]any{
+ "id": "output1",
+ "property": "innerText",
+ "value": "Button1 Pressed @ {{.Payload}}",
+ },
+ },
+ },
+ },
+ {
+ Input: "button2",
+ Processors: []config.ProcessorConfig{
+ {
+ Type: "string.create",
+ Params: map[string]any{
+ "template": "{{.Payload.UnixMilli}}",
+ },
+ },
+ {
+ Type: "debug.log",
+ },
+ {
+ Type: "web.set",
+ Params: map[string]any{
+ "id": "output2",
+ "property": "innerText",
+ "value": "Button2 Pressed @ {{.Payload}}",
+ },
+ },
+ },
+ },
+ },
+ })
+
+ if len(moduleConfigErrors) > 0 {
+ for _, err := range moduleConfigErrors {
+ println("Module config error:", err.Error)
+ }
+ }
+
+ if len(routeConfigErrors) > 0 {
+ for _, err := range routeConfigErrors {
+ println("Route config error:", err.Error)
+ }
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ go func() {
+ router.Start(ctx)
+ fmt.Println("router stopped")
+ }()
+ <-ctx.Done()
+}
diff --git a/internal/module/db-sqlite.go b/internal/module/db-sqlite.go
index 1be8beb..ac2d1e0 100644
--- a/internal/module/db-sqlite.go
+++ b/internal/module/db-sqlite.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/http-server.go b/internal/module/http-server.go
index ddb3d36..360b736 100644
--- a/internal/module/http-server.go
+++ b/internal/module/http-server.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/mqtt-client.go b/internal/module/mqtt-client.go
index 31b8c5e..8853777 100644
--- a/internal/module/mqtt-client.go
+++ b/internal/module/mqtt-client.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/nats-client.go b/internal/module/nats-client.go
index 20e869b..ff3109a 100644
--- a/internal/module/nats-client.go
+++ b/internal/module/nats-client.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/nats-server.go b/internal/module/nats-server.go
index d1b714a..c99aa01 100644
--- a/internal/module/nats-server.go
+++ b/internal/module/nats-server.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/psn-client.go b/internal/module/psn-client.go
index ebcec91..68a9937 100644
--- a/internal/module/psn-client.go
+++ b/internal/module/psn-client.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/redis-client.go b/internal/module/redis-client.go
index 2fafc98..b02e213 100644
--- a/internal/module/redis-client.go
+++ b/internal/module/redis-client.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/sip-call-server.go b/internal/module/sip-call-server.go
index 5c93aaf..d619f7b 100644
--- a/internal/module/sip-call-server.go
+++ b/internal/module/sip-call-server.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/sip-dtmf-server.go b/internal/module/sip-dtmf-server.go
index e562d3f..4d19f69 100644
--- a/internal/module/sip-dtmf-server.go
+++ b/internal/module/sip-dtmf-server.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/tcp-client.go b/internal/module/tcp-client.go
index 4e759fd..affc165 100644
--- a/internal/module/tcp-client.go
+++ b/internal/module/tcp-client.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/tcp-server.go b/internal/module/tcp-server.go
index a3e40a7..e85c808 100644
--- a/internal/module/tcp-server.go
+++ b/internal/module/tcp-server.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/time-timer.go b/internal/module/time-timer.go
index f765013..51dc650 100644
--- a/internal/module/time-timer.go
+++ b/internal/module/time-timer.go
@@ -64,7 +64,7 @@ func (t *TimeTimer) Start(ctx context.Context) error {
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
- return errors.New("net.tcp.client unable to get router from context")
+ return errors.New("time.timer unable to get router from context")
}
t.router = router
moduleContext, cancel := context.WithCancel(ctx)
diff --git a/internal/module/udp-client.go b/internal/module/udp-client.go
index 29e4bd9..3e6553d 100644
--- a/internal/module/udp-client.go
+++ b/internal/module/udp-client.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/udp-multicast.go b/internal/module/udp-multicast.go
index 0353ce0..e8e9a19 100644
--- a/internal/module/udp-multicast.go
+++ b/internal/module/udp-multicast.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/udp-server.go b/internal/module/udp-server.go
index 3815784..b9c80a7 100644
--- a/internal/module/udp-server.go
+++ b/internal/module/udp-server.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package module
import (
diff --git a/internal/module/web-onclick.go b/internal/module/web-onclick.go
new file mode 100644
index 0000000..58d3283
--- /dev/null
+++ b/internal/module/web-onclick.go
@@ -0,0 +1,91 @@
+//go:build js
+
+package module
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "syscall/js"
+ "time"
+
+ "github.com/google/jsonschema-go/jsonschema"
+ "github.com/jwetzell/showbridge-go/internal/common"
+ "github.com/jwetzell/showbridge-go/internal/config"
+)
+
+type WebOnClick struct {
+ config config.ModuleConfig
+ ctx context.Context
+ router common.RouteIO
+ logger *slog.Logger
+ ElementId string
+}
+
+func init() {
+ RegisterModule(ModuleRegistration{
+ Type: "web.onclick",
+ Title: "On Click",
+ ParamsSchema: &jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "id": {
+ Title: "Element ID",
+ Type: "string",
+ Description: "ID of the HTML element to attach the click listener to",
+ },
+ },
+ Required: []string{"duration"},
+ AdditionalProperties: nil,
+ },
+ New: func(config config.ModuleConfig) (common.Module, error) {
+ params := config.Params
+
+ idString, err := params.GetString("id")
+ if err != nil {
+ return nil, fmt.Errorf("web.onclick id error: %w", err)
+ }
+
+ return &WebOnClick{ElementId: idString, config: config, logger: CreateLogger(config)}, nil
+ },
+ })
+}
+
+func (woc *WebOnClick) Id() string {
+ return woc.config.Id
+}
+
+func (woc *WebOnClick) Type() string {
+ return woc.config.Type
+}
+
+func (woc *WebOnClick) Start(ctx context.Context) error {
+ woc.logger.Debug("running")
+ router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
+
+ if !ok {
+ return errors.New("net.tcp.client unable to get router from context")
+ }
+ woc.router = router
+ woc.ctx = ctx
+
+ element := js.Global().Get("document").Call("getElementById", woc.ElementId)
+
+ if element.IsNull() || element.IsUndefined() {
+ return fmt.Errorf("web.onclick unable to find element with id: %s", woc.ElementId)
+ }
+
+ element.Set("onclick", js.FuncOf(func(js.Value, []js.Value) interface{} {
+ if woc.router != nil {
+ woc.router.HandleInput(woc.ctx, woc.Id(), time.Now())
+ }
+ return nil
+ }))
+
+ <-ctx.Done()
+ return nil
+}
+
+func (woc *WebOnClick) Stop() {
+}
diff --git a/internal/processor/artnet-packet-decode.go b/internal/processor/artnet-packet-decode.go
index 692f422..f2abf9f 100644
--- a/internal/processor/artnet-packet-decode.go
+++ b/internal/processor/artnet-packet-decode.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/artnet-packet-encode.go b/internal/processor/artnet-packet-encode.go
index 1a22ce7..0970d7d 100644
--- a/internal/processor/artnet-packet-encode.go
+++ b/internal/processor/artnet-packet-encode.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/http-request-do.go b/internal/processor/http-request-do.go
index 1b89d98..a51f5d6 100644
--- a/internal/processor/http-request-do.go
+++ b/internal/processor/http-request-do.go
@@ -21,6 +21,11 @@ type HTTPRequestDo struct {
URL *template.Template
}
+type HTTPResponse struct {
+ Status int
+ Body []byte
+}
+
func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
diff --git a/internal/processor/http-response-create.go b/internal/processor/http-response-create.go
index 2a720f8..03f1fb0 100644
--- a/internal/processor/http-response-create.go
+++ b/internal/processor/http-response-create.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
@@ -17,11 +19,6 @@ type HTTPResponseCreate struct {
config config.ProcessorConfig
}
-type HTTPResponse struct {
- Status int
- Body []byte
-}
-
func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
diff --git a/internal/processor/mqtt-message-create.go b/internal/processor/mqtt-message-create.go
index b0bf7cf..359c158 100644
--- a/internal/processor/mqtt-message-create.go
+++ b/internal/processor/mqtt-message-create.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/nats-message-create.go b/internal/processor/nats-message-create.go
index c7210ae..0ca993f 100644
--- a/internal/processor/nats-message-create.go
+++ b/internal/processor/nats-message-create.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/script-js.go b/internal/processor/script-js.go
index bbd35f1..7262dc0 100644
--- a/internal/processor/script-js.go
+++ b/internal/processor/script-js.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/script-wasm.go b/internal/processor/script-wasm.go
index daaa76f..bc81111 100644
--- a/internal/processor/script-wasm.go
+++ b/internal/processor/script-wasm.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/sip-response-audio-create.go b/internal/processor/sip-response-audio-create.go
index 91c1dd6..5f92f67 100644
--- a/internal/processor/sip-response-audio-create.go
+++ b/internal/processor/sip-response-audio-create.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/sip-response-dtmf-create.go b/internal/processor/sip-response-dtmf-create.go
index a836bf3..3b48f74 100644
--- a/internal/processor/sip-response-dtmf-create.go
+++ b/internal/processor/sip-response-dtmf-create.go
@@ -1,3 +1,5 @@
+//go:build !js
+
package processor
import (
diff --git a/internal/processor/web-set.go b/internal/processor/web-set.go
new file mode 100644
index 0000000..258f5a4
--- /dev/null
+++ b/internal/processor/web-set.go
@@ -0,0 +1,102 @@
+//go:build js
+
+package processor
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "html/template"
+ "log/slog"
+ "syscall/js"
+
+ "github.com/google/jsonschema-go/jsonschema"
+ "github.com/jwetzell/showbridge-go/internal/common"
+ "github.com/jwetzell/showbridge-go/internal/config"
+)
+
+type WebSet struct {
+ config config.ProcessorConfig
+ ModuleId string
+ ElementId string
+ Property string
+ Value *template.Template
+ logger *slog.Logger
+}
+
+func (kvs *WebSet) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
+
+ element := js.Global().Get("document").Call("getElementById", kvs.ElementId)
+
+ if element.IsNull() || element.IsUndefined() {
+ wrappedPayload.End = true
+ return wrappedPayload, fmt.Errorf("web.set unable to find element with id: %s", kvs.ElementId)
+ }
+
+ var valueBuffer bytes.Buffer
+ err := kvs.Value.Execute(&valueBuffer, wrappedPayload)
+
+ if err != nil {
+ wrappedPayload.End = true
+ return wrappedPayload, err
+ }
+
+ element.Set(kvs.Property, valueBuffer.String())
+ return wrappedPayload, nil
+}
+
+func (kvs *WebSet) Type() string {
+ return kvs.config.Type
+}
+
+func init() {
+ RegisterProcessor(ProcessorRegistration{
+ Type: "web.set",
+ Title: "Set Web Element Property",
+ ParamsSchema: &jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "id": {
+ Title: "Element ID",
+ Type: "string",
+ },
+ "property": {
+ Title: "Property",
+ Type: "string",
+ },
+ "value": {
+ Title: "Value",
+ Type: "string",
+ },
+ },
+ Required: []string{"id", "property", "value"},
+ AdditionalProperties: nil,
+ },
+ New: func(config config.ProcessorConfig) (Processor, error) {
+
+ params := config.Params
+
+ idString, err := params.GetString("id")
+ if err != nil {
+ return nil, fmt.Errorf("web.set id error: %w", err)
+ }
+
+ propertyString, err := params.GetString("property")
+ if err != nil {
+ return nil, fmt.Errorf("web.set property error: %w", err)
+ }
+
+ valueString, err := params.GetString("value")
+ if err != nil {
+ return nil, fmt.Errorf("web.set value error: %w", err)
+ }
+ valueTemplate, err := template.New("template").Parse(valueString)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &WebSet{config: config, ElementId: idString, Property: propertyString, Value: valueTemplate, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
+ },
+ })
+}