nightsky.gno
8.95 Kb · 300 lines
1package nightsky
2
3import (
4 "chain"
5 "strings"
6 "time"
7
8 "gno.land/p/moul/md/v0"
9 "gno.land/p/nt/avl/v0"
10 "gno.land/p/nt/ufmt/v0"
11 nightsky "gno.land/p/nym-vikbez000/nightsky/v1"
12 "gno.land/r/sys/users"
13)
14
15// maxRecentCaptures is the number of captures kept in the network-wide feed.
16const maxRecentCaptures = 50
17
18var (
19 // Registry of all telescopes in the network, keyed by realm pkgPath.
20 // An avl.Tree is used over a map: the registry grows over time, only the
21 // search path is loaded on access, and iteration is in deterministic
22 // (sorted) key order — important for stable Render output.
23 telescopes = avl.NewTree()
24
25 // Recent captures from all telescopes (bounded, newest-first).
26 recentCaptures = nightsky.NewCaptureFeed(maxRecentCaptures)
27)
28
29// Register registers a telescope realm in the NightSky network.
30// Call from your realm's init() function:
31//
32// nightsky.Register(cross(cur), config)
33func Register(cur realm, cfg nightsky.TelescopeConfig) {
34 submission := cur.Previous()
35 pkgPath := submission.PkgPath()
36
37 if submission.IsUser() {
38 panic("Register must be called from realm code, not a user transaction")
39 }
40
41 if telescopes.Has(pkgPath) {
42 return // already registered, silently ignore duplicate
43 }
44
45 if cfg.Name == "" || cfg.Model == "" || cfg.Owner == "" {
46 panic("telescope name, model, and owner cannot be empty")
47 }
48
49 cfg.RealmPath = pkgPath
50
51 telescopes.Set(pkgPath, cfg)
52
53 chain.Emit("TelescopeRegistered", "pkgPath", pkgPath, "name", cfg.Name, "owner", cfg.Owner)
54}
55
56// UpdateTelescopeStatus allows a registered telescope realm to update its status in the registry.
57// Call from your realm: nightsky.UpdateTelescopeStatus(cross(cur), "online")
58func UpdateTelescopeStatus(cur realm, status string) {
59 submission := cur.Previous()
60 pkgPath := submission.PkgPath()
61
62 if submission.IsUser() {
63 panic("must be called from realm code")
64 }
65
66 config := mustGetTelescope(pkgPath)
67 config.UpdateStatus(status)
68 telescopes.Set(pkgPath, config)
69
70 chain.Emit("StatusUpdate", "pkgPath", pkgPath, "status", status)
71}
72
73// SubmitCapture allows a registered telescope realm to add a capture to the network feed.
74// Call from your realm: nightsky.SubmitCapture(cross(cur), imageURL, ra, dec, exposure, capturedBy)
75func SubmitCapture(cur realm, imageURL string, targetRA, targetDec float64, exposure int, capturedBy string) {
76 submission := cur.Previous()
77 pkgPath := submission.PkgPath()
78
79 if submission.IsUser() {
80 panic("must be called from realm code")
81 }
82
83 config := mustGetTelescope(pkgPath)
84
85 if imageURL == "" {
86 panic("image URL cannot be empty")
87 }
88
89 recentCaptures.Add(nightsky.CaptureResult{
90 ImageURL: imageURL,
91 TargetRA: targetRA,
92 TargetDec: targetDec,
93 Exposure: exposure,
94 CapturedBy: capturedBy,
95 CapturedAt: time.Now(),
96 TelescopeID: config.Name,
97 })
98
99 chain.Emit("NetworkCapture", "pkgPath", pkgPath, "telescope", config.Name, "imageURL", imageURL, "capturedBy", capturedBy)
100}
101
102// UnregisterTelescope removes a telescope realm from the network.
103// Call from your realm: nightsky.UnregisterTelescope(cross(cur))
104func UnregisterTelescope(cur realm) {
105 submission := cur.Previous()
106 pkgPath := submission.PkgPath()
107
108 if submission.IsUser() {
109 panic("must be called from realm code")
110 }
111
112 if _, removed := telescopes.Remove(pkgPath); !removed {
113 panic("telescope not registered")
114 }
115
116 chain.Emit("TelescopeUnregistered", "pkgPath", pkgPath)
117}
118
119// mustGetTelescope returns the config for pkgPath or panics if not registered.
120func mustGetTelescope(pkgPath string) nightsky.TelescopeConfig {
121 if !telescopes.Has(pkgPath) {
122 panic("telescope not registered")
123 }
124 return telescopes.Get(pkgPath).(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/nightsky/v1:telescopes") + "\n\n"
183 out += md.Link("Recent Captures", "/r/nym-vikbez000/nightsky/v1:captures") + "\n\n"
184 out += md.Link("Telescope Map", "/r/nym-vikbez000/nightsky/v1:map") + "\n\n"
185
186 if latest, ok := recentCaptures.Latest(); ok {
187 out += md.H2("Latest Capture")
188 out += "\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/nightsky/v1` 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/nightsky/v1") + "\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", strings.TrimPrefix(config.RealmPath, "gno.land")))
237 out += md.HorizontalRule()
238 return false
239 })
240
241 out += md.Link("← Back to main page", "/r/nym-vikbez000/nightsky/v1") + "\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/nightsky/v1") + "\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/nightsky/v1") + "\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/nightsky/v1") + "\n\n"
298
299 return out
300}