Search Apps Documentation Source Content File Folder Download Copy Actions Download

nightsky.gno

8.90 Kb · 300 lines
  1package nightsky
  2
  3import (
  4	"chain"
  5	"time"
  6
  7	"gno.land/p/moul/md"
  8	"gno.land/p/nt/avl/v0"
  9	"gno.land/p/nt/ufmt/v0"
 10	nightsky "gno.land/p/nym-vikbez000/nightsky0"
 11	"gno.land/r/sys/users"
 12)
 13
 14// maxRecentCaptures is the number of captures kept in the network-wide feed.
 15const maxRecentCaptures = 50
 16
 17var (
 18	// Registry of all telescopes in the network, keyed by realm pkgPath.
 19	// An avl.Tree is used over a map: the registry grows over time, only the
 20	// search path is loaded on access, and iteration is in deterministic
 21	// (sorted) key order — important for stable Render output.
 22	telescopes = avl.NewTree()
 23
 24	// Recent captures from all telescopes (bounded, newest-first).
 25	recentCaptures = nightsky.NewCaptureFeed(maxRecentCaptures)
 26)
 27
 28// Register registers a telescope realm in the NightSky network.
 29// Call from your realm's init() function:
 30//
 31//	nightsky.Register(cross(cur), config)
 32func Register(cur realm, cfg nightsky.TelescopeConfig) {
 33	submission := cur.Previous()
 34	pkgPath := submission.PkgPath()
 35
 36	if submission.IsUser() {
 37		panic("Register must be called from realm code, not a user transaction")
 38	}
 39
 40	if telescopes.Has(pkgPath) {
 41		return // already registered, silently ignore duplicate
 42	}
 43
 44	if cfg.Name == "" || cfg.Model == "" || cfg.Owner == "" {
 45		panic("telescope name, model, and owner cannot be empty")
 46	}
 47
 48	cfg.RealmPath = pkgPath
 49
 50	telescopes.Set(pkgPath, cfg)
 51
 52	chain.Emit("TelescopeRegistered", "pkgPath", pkgPath, "name", cfg.Name, "owner", cfg.Owner)
 53}
 54
 55// UpdateTelescopeStatus allows a registered telescope realm to update its status in the registry.
 56// Call from your realm: nightsky.UpdateTelescopeStatus(cross(cur), "online")
 57func UpdateTelescopeStatus(cur realm, status string) {
 58	submission := cur.Previous()
 59	pkgPath := submission.PkgPath()
 60
 61	if submission.IsUser() {
 62		panic("must be called from realm code")
 63	}
 64
 65	config := mustGetTelescope(pkgPath)
 66	config.UpdateStatus(status)
 67	telescopes.Set(pkgPath, config)
 68
 69	chain.Emit("StatusUpdate", "pkgPath", pkgPath, "status", status)
 70}
 71
 72// SubmitCapture allows a registered telescope realm to add a capture to the network feed.
 73// Call from your realm: nightsky.SubmitCapture(cross(cur), imageURL, ra, dec, exposure, capturedBy)
 74func SubmitCapture(cur realm, imageURL string, targetRA, targetDec float64, exposure int, capturedBy string) {
 75	submission := cur.Previous()
 76	pkgPath := submission.PkgPath()
 77
 78	if submission.IsUser() {
 79		panic("must be called from realm code")
 80	}
 81
 82	config := mustGetTelescope(pkgPath)
 83
 84	if imageURL == "" {
 85		panic("image URL cannot be empty")
 86	}
 87
 88	recentCaptures.Add(nightsky.CaptureResult{
 89		ImageURL:    imageURL,
 90		TargetRA:    targetRA,
 91		TargetDec:   targetDec,
 92		Exposure:    exposure,
 93		CapturedBy:  capturedBy,
 94		CapturedAt:  time.Now(),
 95		TelescopeID: config.Name,
 96	})
 97
 98	chain.Emit("NetworkCapture", "pkgPath", pkgPath, "telescope", config.Name, "imageURL", imageURL, "capturedBy", capturedBy)
 99}
100
101// UnregisterTelescope removes a telescope realm from the network.
102// Call from your realm: nightsky.UnregisterTelescope(cross(cur))
103func UnregisterTelescope(cur realm) {
104	submission := cur.Previous()
105	pkgPath := submission.PkgPath()
106
107	if submission.IsUser() {
108		panic("must be called from realm code")
109	}
110
111	if _, removed := telescopes.Remove(pkgPath); !removed {
112		panic("telescope not registered")
113	}
114
115	chain.Emit("TelescopeUnregistered", "pkgPath", pkgPath)
116}
117
118// mustGetTelescope returns the config for pkgPath or panics if not registered.
119func mustGetTelescope(pkgPath string) nightsky.TelescopeConfig {
120	value, exists := telescopes.Get(pkgPath)
121	if !exists {
122		panic("telescope not registered")
123	}
124	return value.(nightsky.TelescopeConfig)
125}
126
127// GetTelescopeCount returns the number of registered telescopes
128func GetTelescopeCount() int {
129	return telescopes.Size()
130}
131
132// GetOnlineTelescopeCount returns the number of online telescopes
133func GetOnlineTelescopeCount() int {
134	count := 0
135	telescopes.Iterate("", "", func(_ string, value any) bool {
136		if value.(nightsky.TelescopeConfig).Status == "online" {
137			count++
138		}
139		return false
140	})
141	return count
142}
143
144func usernameOrAddressLink(addr string) string {
145	user := users.ResolveAddress(address(addr))
146	if user != nil {
147		return user.RenderLink("")
148	}
149	return addr
150}
151
152func Render(path string) string {
153	if path == "telescopes" {
154		return renderTelescopeList()
155	}
156
157	if path == "captures" {
158		return renderRecentCaptures()
159	}
160
161	if path == "map" {
162		return renderTelescopeMap()
163	}
164
165	return renderMainPage()
166}
167
168func renderMainPage() string {
169	out := ""
170	out += md.H1("Gno NightSky 🔭")
171	out += "Decentralized telescope network for stargazing\n\n"
172	out += "Connect your smart telescope to the Gno.land blockchain and share access to the stars!\n\n"
173
174	out += md.H2("Network Status")
175	totalTelescopes := GetTelescopeCount()
176	onlineTelescopes := GetOnlineTelescopeCount()
177	out += ufmt.Sprintf("🔭 Total Telescopes: %d\n\n", totalTelescopes)
178	out += ufmt.Sprintf("🟢 Online: %d\n\n", onlineTelescopes)
179	out += ufmt.Sprintf("🔴 Offline: %d\n\n", totalTelescopes-onlineTelescopes)
180
181	out += md.H2("Quick Links")
182	out += md.Link("View All Telescopes", "/r/nym-vikbez000/nightsky0:telescopes") + "\n\n"
183	out += md.Link("Recent Captures", "/r/nym-vikbez000/nightsky0:captures") + "\n\n"
184	out += md.Link("Telescope Map", "/r/nym-vikbez000/nightsky0:map") + "\n\n"
185
186	if latest, ok := recentCaptures.Latest(); ok {
187		out += md.H2("Latest Capture")
188		out += "![Latest capture](" + latest.ImageURL + ")\n\n"
189		out += ufmt.Sprintf("_RA %.8fh · Dec %.8f° · %ds · %s_\n\n",
190			latest.TargetRA, latest.TargetDec, latest.Exposure, latest.TelescopeID)
191	}
192
193	out += md.HorizontalRule()
194
195	out += md.H2("How to Register Your Telescope")
196	out += "Deploy your own telescope realm using the `/p/nym-vikbez000/nightsky0` package,\n"
197	out += "then call `nightsky.Register(cross(cur), ...)` from your `init()` function.\n\n"
198	out += "You can clone " + md.Link("my telescope realm", "/r/nym-vikbez000/telescope") + " as a starting point.\n\n"
199
200	out += md.H2("About")
201	out += "NightSky enables telescope owners to share access to their equipment through a decentralized network.\n\n"
202	out += "Also take a look at " + md.Link("PiaGno", "https://github.com/gnoverse/piagno") + " 🎹\n\n"
203
204	return out
205}
206
207func renderTelescopeList() string {
208	out := ""
209	out += md.H1("NightSky - Telescope Network")
210
211	if telescopes.Size() == 0 {
212		out += "\nNo telescopes registered yet.\n\n"
213		out += md.Link("← Back to main page", "/r/nym-vikbez000/nightsky0") + "\n\n"
214		return out
215	}
216
217	out += ufmt.Sprintf("\nTotal: %d telescopes\n\n", telescopes.Size())
218
219	telescopes.Iterate("", "", func(_ string, value any) bool {
220		config := value.(nightsky.TelescopeConfig)
221		statusIcon := "🔴"
222		switch config.Status {
223		case "online":
224			statusIcon = "🟢"
225		case "busy":
226			statusIcon = "🟡"
227		case "error":
228			statusIcon = "🔴"
229		}
230
231		out += md.H3(statusIcon + " " + config.Name)
232		out += ufmt.Sprintf("**Model:** %s\n\n", config.Model)
233		out += ufmt.Sprintf("**Owner:** %s\n\n", usernameOrAddressLink(config.Owner))
234		out += ufmt.Sprintf("**Location:** %.4f°, %.4f°\n\n", config.Latitude, config.Longitude)
235		out += ufmt.Sprintf("**Status:** %s\n\n", config.Status)
236		out += ufmt.Sprintf("**Realm:** %s\n\n", md.Link("Visit", config.RealmPath))
237		out += md.HorizontalRule()
238		return false
239	})
240
241	out += md.Link("← Back to main page", "/r/nym-vikbez000/nightsky0") + "\n\n"
242
243	return out
244}
245
246func renderRecentCaptures() string {
247	out := ""
248	out += md.H1("NightSky - Recent Captures")
249
250	if recentCaptures.Len() == 0 {
251		out += "\nNo captures yet.\n\n"
252		out += md.Link("← Back to main page", "/r/nym-vikbez000/nightsky0") + "\n\n"
253		return out
254	}
255
256	out += ufmt.Sprintf("\nShowing %d most recent captures\n\n", recentCaptures.Len())
257
258	i := 0
259	recentCaptures.IterateNewest(func(capture nightsky.CaptureResult) bool {
260		i++
261		out += md.H3(ufmt.Sprintf("Capture #%d", i))
262		out += ufmt.Sprintf("**Telescope:** %s\n\n", capture.TelescopeID)
263		out += ufmt.Sprintf("**Captured by:** %s\n\n", usernameOrAddressLink(capture.CapturedBy))
264		out += ufmt.Sprintf("**Target:** RA %.8fh, Dec %.8f°\n\n", capture.TargetRA, capture.TargetDec)
265		out += ufmt.Sprintf("**Exposure:** %d seconds\n\n", capture.Exposure)
266		out += ufmt.Sprintf("**Image:** %s\n\n", md.Link("View", capture.ImageURL))
267		out += md.HorizontalRule()
268		return false
269	})
270
271	out += md.Link("← Back to main page", "/r/nym-vikbez000/nightsky0") + "\n\n"
272
273	return out
274}
275
276func renderTelescopeMap() string {
277	out := ""
278	out += md.H1("NightSky - Telescope Map")
279	out += "\n📍 Interactive map coming soon!\n\n"
280	out += "For now, here are the telescope locations:\n\n"
281
282	if telescopes.Size() == 0 {
283		out += "No telescopes registered yet.\n\n"
284	} else {
285		telescopes.Iterate("", "", func(_ string, value any) bool {
286			config := value.(nightsky.TelescopeConfig)
287			out += ufmt.Sprintf("- **%s**: %.4f°, %.4f° (%s)\n\n",
288				config.Name,
289				config.Latitude,
290				config.Longitude,
291				config.Status,
292			)
293			return false
294		})
295	}
296
297	out += md.Link("← Back to main page", "/r/nym-vikbez000/nightsky0") + "\n\n"
298
299	return out
300}