mirror of
https://github.com/ditatompel/xmr-remote-nodes.git
synced 2025-01-08 05:52:10 +07:00
Monero remote node UI for frontend
This commit also implement the simple remote node queries. TODO: Add filter for various data
This commit is contained in:
parent
7cd802e640
commit
ca759fc1d0
6 changed files with 297 additions and 24 deletions
|
@ -1,5 +1,82 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { DataHandler } from '@vincjo/datatables/remote';
|
||||||
|
import { format, formatDistance } from 'date-fns';
|
||||||
|
import { loadData } from './api-handler';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import {
|
||||||
|
DtSrRowsPerPage,
|
||||||
|
DtSrThSort,
|
||||||
|
DtSrThFilter,
|
||||||
|
DtSrRowCount,
|
||||||
|
DtSrPagination
|
||||||
|
} from '$lib/components/datatables/server';
|
||||||
|
import {
|
||||||
|
HostPortCell,
|
||||||
|
NetTypeCell,
|
||||||
|
ProtocolCell,
|
||||||
|
CountryCellWithAsn,
|
||||||
|
StatusCell,
|
||||||
|
UptimeCell,
|
||||||
|
EstimateFeeCell
|
||||||
|
} from './components';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
let filterNettype = 'any';
|
||||||
|
let filterProtocol = 'any';
|
||||||
|
let filterCc = 'any';
|
||||||
|
let filterStatus = -1;
|
||||||
|
let checkboxCors = false;
|
||||||
|
|
||||||
|
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||||
|
let rows = handler.getRows();
|
||||||
|
|
||||||
|
const reloadData = () => {
|
||||||
|
handler.invalidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {number | undefined} */
|
||||||
|
let intervalId;
|
||||||
|
let intervalValue = 0;
|
||||||
|
|
||||||
|
const intervalOptions = [
|
||||||
|
{ value: 0, label: 'No' },
|
||||||
|
{ value: 5, label: '5s' },
|
||||||
|
{ value: 10, label: '10s' },
|
||||||
|
{ value: 30, label: '30s' },
|
||||||
|
{ value: 60, label: '1m' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const startInterval = () => {
|
||||||
|
const seconds = intervalValue;
|
||||||
|
if (isNaN(seconds) || seconds < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intervalOptions.some((option) => option.value === seconds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds > 0) {
|
||||||
|
reloadData();
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
reloadData();
|
||||||
|
}, seconds * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: startInterval(); // Automatically start the interval on change
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(intervalId); // Clear the interval when the component is destroyed
|
||||||
|
});
|
||||||
|
onMount(() => {
|
||||||
|
handler.onChange((state) => loadData(state));
|
||||||
|
handler.invalidate();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header id="hero" class="hero-gradient py-7">
|
<header id="hero" class="hero-gradient py-7">
|
||||||
|
@ -36,19 +113,40 @@
|
||||||
<div class="section-container">
|
<div class="section-container">
|
||||||
<div class="space-y-2 overflow-x-auto">
|
<div class="space-y-2 overflow-x-auto">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<!-- <DtSrRowsPerPage {handler} /> -->
|
<DtSrRowsPerPage {handler} />
|
||||||
|
<div class="invisible flex place-items-center md:visible">
|
||||||
|
<label for="autoRefreshInterval">Auto Refresh:</label>
|
||||||
|
<select
|
||||||
|
class="select ml-2"
|
||||||
|
id="autoRefreshInterval"
|
||||||
|
bind:value={intervalValue}
|
||||||
|
on:change={startInterval}
|
||||||
|
>
|
||||||
|
{#each intervalOptions as { value, label }}
|
||||||
|
<option {value}>{label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex place-items-center">
|
||||||
|
<button
|
||||||
|
id="reloadDt"
|
||||||
|
name="reloadDt"
|
||||||
|
class="variant-filled-primary btn"
|
||||||
|
on:click={reloadData}>Reload</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<table class="table table-hover table-compact w-full table-auto">
|
<table class="table table-hover table-compact w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Host:Port</th>
|
<th>Host:Port</th>
|
||||||
<th><label for="fNettype">Nettype</label></th>
|
<th>Nettype</th>
|
||||||
<th><label for="fProtocol">Protocol</label></th>
|
<th>Protocol</th>
|
||||||
<th><label for="fCc">Country</label></th>
|
<th>Country</th>
|
||||||
<th><label for="fStatus">Status</label></th>
|
<th>Status</th>
|
||||||
<th>Est. Fee</th>
|
<th>Est. Fee</th>
|
||||||
|
|
||||||
<DtSrThSort {handler} orderBy="uptime">Uptime</DtSrThSort>
|
<DtSrThSort {handler} orderBy="uptime">Uptime</DtSrThSort>
|
||||||
<DtSrThSort {handler} orderBy="last_checked">Check</DtSrThSort>
|
<DtSrThSort {handler} orderBy="last_checked">Check</DtSrThSort>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -100,6 +198,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="any">Any</option>
|
<option value="any">Any</option>
|
||||||
|
<!--
|
||||||
{#each data.countries as country}
|
{#each data.countries as country}
|
||||||
{#if country.cc === ''}
|
{#if country.cc === ''}
|
||||||
<option value="UNKNOWN">UNKNOWN ({country.total_nodes})</option>
|
<option value="UNKNOWN">UNKNOWN ({country.total_nodes})</option>
|
||||||
|
@ -109,6 +208,7 @@
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
-->
|
||||||
</select>
|
</select>
|
||||||
</th>
|
</th>
|
||||||
<th colspan="2">
|
<th colspan="2">
|
||||||
|
@ -146,7 +246,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each $rows as row}
|
{#each $rows as row (row.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
><HostPortCell
|
><HostPortCell
|
||||||
|
@ -173,23 +273,27 @@
|
||||||
statuses={row.last_check_statuses}
|
statuses={row.last_check_statuses}
|
||||||
/></td
|
/></td
|
||||||
>
|
>
|
||||||
<td
|
<td>
|
||||||
><EstimateFeeCell
|
<!-- <EstimateFeeCell
|
||||||
estimate_fee={row.estimate_fee}
|
estimate_fee={row.estimate_fee}
|
||||||
majority_fee={netFees[row.nettype]}
|
majority_fee={netFees[row.nettype]}
|
||||||
/></td
|
/>
|
||||||
>
|
-->
|
||||||
|
</td>
|
||||||
<td><UptimeCell uptime={row.uptime} /></td>
|
<td><UptimeCell uptime={row.uptime} /></td>
|
||||||
<td>{format(row.last_checked * 1000, 'PP HH:mm')}</td>
|
<td>
|
||||||
|
{format(row.last_checked * 1000, 'PP HH:mm')}<br />
|
||||||
|
{formatDistance(row.last_checked * 1000, new Date(), { addSuffix: true })}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="flex justify-between">
|
|
||||||
|
<div class="flex justify-between mb-2">
|
||||||
<DtSrRowCount {handler} />
|
<DtSrRowCount {handler} />
|
||||||
<DtSrPagination {handler} />
|
<DtSrPagination {handler} />
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { PUBLIC_API_ENDPOINT } from '$env/static/public';
|
import { apiUri } from '$lib/utils/common';
|
||||||
|
|
||||||
/** @param {import('@vincjo/datatables/remote/state')} state */
|
/** @param {import('@vincjo/datatables/remote/state')} state */
|
||||||
export async function loadApiData(state) {
|
export const loadData = async (state) => {
|
||||||
const response = await fetch(`${PUBLIC_API_ENDPOINT}/monero/remote-node-dt?${getParams(state)}`);
|
const response = await fetch(apiUri(`/api/v1/nodes?${getParams(state)}`));
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
state.setTotalRows(json.data.total_rows ?? 0);
|
||||||
|
return json.data.items ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
state.setTotalRows(json.data.total ?? 0);
|
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||||
|
|
||||||
return json.data.nodes ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParams = ({ pageNumber, offset, rowsPerPage, sort, filters }) => {
|
|
||||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||||
|
|
||||||
if (sort) {
|
if (sort) {
|
||||||
params += `&sort=${sort.orderBy}&dir=${sort.direction}`;
|
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||||
}
|
}
|
||||||
if (filters) {
|
if (filters) {
|
||||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||||
|
|
|
@ -115,6 +115,32 @@ func Prober(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MoneroNodes(c *fiber.Ctx) error {
|
||||||
|
moneroRepo := repo.NewMoneroRepo(database.GetDB())
|
||||||
|
query := repo.MoneroQueryParams{
|
||||||
|
RowsPerPage: c.QueryInt("limit", 10),
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
SortBy: c.Query("sort_by", "id"),
|
||||||
|
SortDirection: c.Query("sort_direction", "desc"),
|
||||||
|
Host: c.Query("host"),
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := moneroRepo.Nodes(query)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"status": "error",
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Success",
|
||||||
|
"data": nodes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AddNode(c *fiber.Ctx) error {
|
func AddNode(c *fiber.Ctx) error {
|
||||||
formPort := c.FormValue("port")
|
formPort := c.FormValue("port")
|
||||||
port, err := strconv.Atoi(formPort)
|
port, err := strconv.Atoi(formPort)
|
||||||
|
|
|
@ -14,6 +14,7 @@ func V1Api(app *fiber.App) {
|
||||||
|
|
||||||
v1.Get("/prober", Prober)
|
v1.Get("/prober", Prober)
|
||||||
v1.Post("/prober", Prober)
|
v1.Post("/prober", Prober)
|
||||||
|
v1.Get("/nodes", MoneroNodes)
|
||||||
v1.Post("/nodes", AddNode)
|
v1.Post("/nodes", AddNode)
|
||||||
v1.Get("/crons", Crons)
|
v1.Get("/crons", Crons)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,20 @@ package repo
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ditatompel/xmr-nodes/internal/database"
|
"github.com/ditatompel/xmr-nodes/internal/database"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MoneroRepository interface {
|
type MoneroRepository interface {
|
||||||
Add(protocol string, host string, port uint) error
|
Add(protocol string, host string, port uint) error
|
||||||
|
Nodes(q MoneroQueryParams) (MoneroNodes, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MoneroRepo struct {
|
type MoneroRepo struct {
|
||||||
|
@ -22,6 +27,111 @@ func NewMoneroRepo(db *database.DB) MoneroRepository {
|
||||||
return &MoneroRepo{db}
|
return &MoneroRepo{db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MoneroNode struct {
|
||||||
|
Id uint `json:"id,omitempty" db:"id"`
|
||||||
|
Hostname string `json:"hostname" db:"hostname"`
|
||||||
|
Ip string `json:"ip" db:"ip_addr"`
|
||||||
|
Port uint `json:"port" db:"port"`
|
||||||
|
Protocol string `json:"protocol" db:"protocol"`
|
||||||
|
IsTor bool `json:"is_tor" db:"is_tor"`
|
||||||
|
IsAvailable bool `json:"is_available" db:"is_available"`
|
||||||
|
NetType string `json:"nettype" db:"nettype"`
|
||||||
|
LastHeight uint `json:"last_height" db:"last_height"`
|
||||||
|
AdjustedTime uint `json:"adjusted_time" db:"adjusted_time"`
|
||||||
|
DatabaseSize uint `json:"database_size" db:"database_size"`
|
||||||
|
Difficulty uint `json:"difficulty" db:"difficulty"`
|
||||||
|
NodeVersion string `json:"node_version" db:"node_version"`
|
||||||
|
Uptime float32 `json:"uptime" db:"uptime"`
|
||||||
|
EstimateFee uint `json:"estimate_fee" db:"estimate_fee"`
|
||||||
|
Asn uint `json:"asn" db:"asn"`
|
||||||
|
AsnName string `json:"asn_name" db:"asn_name"`
|
||||||
|
CountryCode string `json:"cc" db:"country"`
|
||||||
|
CountryName string `json:"country_name" db:"country_name"`
|
||||||
|
City string `json:"city" db:"city"`
|
||||||
|
Lat float64 `json:"latitude" db:"lat"`
|
||||||
|
Lon float64 `json:"longitude" db:"lon"`
|
||||||
|
DateEntered uint `json:"date_entered,omitempty" db:"date_entered"`
|
||||||
|
LastChecked uint `json:"last_checked" db:"last_checked"`
|
||||||
|
FailedCount uint `json:"failed_count,omitempty" db:"failed_count"`
|
||||||
|
LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"`
|
||||||
|
CorsCapable bool `json:"cors" db:"cors_capable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoneroNodes struct {
|
||||||
|
TotalRows int `json:"total_rows"`
|
||||||
|
RowsPerPage int `json:"rows_per_page"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
NextPage int `json:"next_page"`
|
||||||
|
Items []*MoneroNode `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoneroQueryParams struct {
|
||||||
|
Host string
|
||||||
|
RowsPerPage int
|
||||||
|
Page int
|
||||||
|
SortBy string
|
||||||
|
SortDirection string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *MoneroRepo) Nodes(q MoneroQueryParams) (MoneroNodes, error) {
|
||||||
|
queryParams := []interface{}{}
|
||||||
|
whereQueries := []string{}
|
||||||
|
where := ""
|
||||||
|
|
||||||
|
if q.Host != "" {
|
||||||
|
whereQueries = append(whereQueries, "(hostname LIKE ? OR ip_addr LIKE ?)")
|
||||||
|
queryParams = append(queryParams, "%"+q.Host+"%")
|
||||||
|
queryParams = append(queryParams, "%"+q.Host+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(whereQueries) > 0 {
|
||||||
|
where = "WHERE " + strings.Join(whereQueries, " AND ")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := MoneroNodes{}
|
||||||
|
|
||||||
|
queryTotalRows := fmt.Sprintf("SELECT COUNT(id) AS total_rows FROM tbl_node %s", where)
|
||||||
|
|
||||||
|
err := repo.db.QueryRow(queryTotalRows, queryParams...).Scan(&nodes.TotalRows)
|
||||||
|
if err != nil {
|
||||||
|
return nodes, err
|
||||||
|
}
|
||||||
|
queryParams = append(queryParams, q.RowsPerPage, (q.Page-1)*q.RowsPerPage)
|
||||||
|
|
||||||
|
allowedSort := []string{"last_checked", "uptime"}
|
||||||
|
sortBy := "last_checked"
|
||||||
|
if slices.Contains(allowedSort, q.SortBy) {
|
||||||
|
sortBy = q.SortBy
|
||||||
|
}
|
||||||
|
sortDirection := "DESC"
|
||||||
|
if q.SortDirection == "asc" {
|
||||||
|
sortDirection = "ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf("SELECT id, protocol, hostname, port, is_tor, is_available, nettype, last_height, adjusted_time, database_size, difficulty, node_version, uptime, estimate_fee, ip_addr, asn, asn_name, country, country_name, city, lat, lon, date_entered, last_checked, last_check_status, cors_capable FROM tbl_node %s ORDER BY %s %s LIMIT ? OFFSET ?", where, sortBy, sortDirection)
|
||||||
|
|
||||||
|
row, err := repo.db.Query(query, queryParams...)
|
||||||
|
if err != nil {
|
||||||
|
return nodes, err
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
|
||||||
|
nodes.RowsPerPage = q.RowsPerPage
|
||||||
|
nodes.CurrentPage = q.Page
|
||||||
|
nodes.NextPage = q.Page + 1
|
||||||
|
|
||||||
|
for row.Next() {
|
||||||
|
node := MoneroNode{}
|
||||||
|
err = row.Scan(&node.Id, &node.Protocol, &node.Hostname, &node.Port, &node.IsTor, &node.IsAvailable, &node.NetType, &node.LastHeight, &node.AdjustedTime, &node.DatabaseSize, &node.Difficulty, &node.NodeVersion, &node.Uptime, &node.EstimateFee, &node.Ip, &node.Asn, &node.AsnName, &node.CountryName, &node.CountryCode, &node.City, &node.Lat, &node.Lon, &node.DateEntered, &node.LastChecked, &node.LastCheckStatus, &node.CorsCapable)
|
||||||
|
if err != nil {
|
||||||
|
return nodes, err
|
||||||
|
}
|
||||||
|
nodes.Items = append(nodes.Items, &node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (repo *MoneroRepo) Add(protocol string, hostname string, port uint) error {
|
func (repo *MoneroRepo) Add(protocol string, hostname string, port uint) error {
|
||||||
if protocol != "http" && protocol != "https" {
|
if protocol != "http" && protocol != "https" {
|
||||||
return errors.New("Invalid protocol, must one of or HTTP/HTTPS")
|
return errors.New("Invalid protocol, must one of or HTTP/HTTPS")
|
||||||
|
|
|
@ -39,6 +39,39 @@ CREATE TABLE `tbl_cron` (
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
DROP TABLE IF EXISTS `tbl_node`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8 */;
|
||||||
|
CREATE TABLE `tbl_node` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`protocol` varchar(6) NOT NULL DEFAULT 'http' COMMENT 'http | https',
|
||||||
|
`hostname` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`port` int(6) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`is_tor` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`is_available` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`nettype` varchar(100) NOT NULL COMMENT 'mainnet | stagenet | testnet',
|
||||||
|
`last_height` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`adjusted_time` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`database_size` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`difficulty` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`node_version` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`uptime` float(5,2) unsigned NOT NULL DEFAULT 0.00,
|
||||||
|
`estimate_fee` int(9) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`ip_addr` varchar(200) NOT NULL,
|
||||||
|
`asn` int(9) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`asn_name` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`country` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`country_name` varchar(255) NOT NULL DEFAULT '',
|
||||||
|
`city` varchar(200) NOT NULL DEFAULT '',
|
||||||
|
`lat` float NOT NULL DEFAULT 0 COMMENT 'latitude',
|
||||||
|
`lon` float NOT NULL DEFAULT 0 COMMENT 'longitude',
|
||||||
|
`date_entered` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`last_checked` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`last_check_status` text DEFAULT NULL,
|
||||||
|
`cors_capable` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
DROP TABLE IF EXISTS `tbl_prober`;
|
DROP TABLE IF EXISTS `tbl_prober`;
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
/*!40101 SET character_set_client = utf8 */;
|
/*!40101 SET character_set_client = utf8 */;
|
||||||
|
|
Loading…
Reference in a new issue