Oasis 0.8.7 mobile (nodejs-project baseline)

This commit is contained in:
s1to 2026-07-09 09:56:30 +02:00
commit e563b86ede
226 changed files with 141968 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<title>oasis favicon</title>
<text x="0" y="14">🏝️</text>
</svg>

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View file

@ -0,0 +1,92 @@
{
"academia": {
"name": "ACADEMIA",
"short": "aca",
"motto": "Nosce te ipsum…",
"roles": "Educators, teachers, coordinators",
"function": "Education, understanding, talent discovery, selection process",
"description": "All inhabitants start here. ACADEMIA receives newcomers, provides information on the simulation mechanics, presents each house's objectives and runs the selection process.",
"month": 1,
"image": "/assets/larp/images/academia.jpg"
},
"solaris": {
"name": "SolarIS",
"short": "sol",
"motto": "The word makes us human. Violence, beasts.",
"roles": "Rulers, lawyers, diplomats",
"function": "Governance, law, dialogue, politics, communication",
"description": "Holds the political voice. Mediates disputes, drafts norms, builds coalitions through dialogue and persuasion.",
"month": 2,
"image": "/assets/larp/images/solaris.jpg"
},
"arrakis": {
"name": "ARRakis",
"short": "arr",
"motto": "How long do you say that it must be resolved?",
"roles": "Engineers, scientists, technicians",
"function": "Technology, invention, problem-solving, building and maintaining",
"description": "The makers. They keep the machinery running, prototype, repair, automate; favour pragmatic solutions over endless debate.",
"month": 3,
"image": "/assets/larp/images/arrakis.jpg"
},
"terraverde": {
"name": "TERRA.VErDE",
"short": "ter",
"motto": "Love nature, universe and being, because we are all one.",
"roles": "Farmers, ecologists, doctors, nutritionists",
"function": "Healthcare, food, nature, resources, climate management",
"description": "Stewards of the living systems. Manage food, health and the relationship with the biosphere; long timescales, regenerative principles.",
"month": 4,
"image": "/assets/larp/images/terraverde.jpg"
},
"unsystem": {
"name": "UNSYSTem",
"short": "uns",
"motto": "We do not make statements!",
"roles": "Chaos agents, trolls, punks",
"function": "Organization of disorder, tactical chaos",
"description": "Holds the right to question, sabotage, provoke. Resists capture by any other house and keeps the system uncomfortable.",
"month": 5,
"image": "/assets/larp/images/unsystem.jpg"
},
"dogma": {
"name": "DogmA",
"short": "dog",
"motto": "The question is not when to do it, the question is how.",
"roles": "Thinkers, journalists, philosophers",
"function": "Information control, knowledge, philosophy, memory, AI",
"description": "The archive and the discourse. Curates knowledge, drafts narratives, governs how information is produced and propagated.",
"month": 6,
"image": "/assets/larp/images/dogma.jpg"
},
"helix": {
"name": "HeliX",
"short": "hlx",
"motto": "If you do not smile, it's not my revolution.",
"roles": "Clowns, influencers, musicians, priests",
"function": "Entertainment, culture, humor, celebration, human relationships",
"description": "Carries culture, ritual and joy. Holds the social fabric together through art, festival and shared emotion.",
"month": 7,
"image": "/assets/larp/images/helix.jpg"
},
"quark": {
"name": "QuarK",
"short": "quk",
"motto": "Without security, there is no freedom!",
"roles": "Athletes, soldiers, street people",
"function": "Protection, security, defense, survival, family",
"description": "The guard. Trains, defends, organises mutual aid; demands clear loyalties and clear lines.",
"month": 8,
"image": "/assets/larp/images/quark.jpg"
},
"hermandad": {
"name": "HERmanDAD",
"short": "hrm",
"motto": "Grow. Build. Expand!",
"roles": "Architects, builders, investors, industrialists",
"function": "Construction, development, sustainability, logistics, production",
"description": "The builders. Plan large works, organise production lines and supply chains; measure success in things shipped and capacity created.",
"month": 9,
"image": "/assets/larp/images/hermandad.jpg"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,67 @@
.hljs-comment,
.hljs-quote {
color: var(--base03);
}
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: var(--base08);
}
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: var(--base09);
}
.hljs-attribute {
color: var(--base0A);
}
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: var(--base0B);
}
.hljs-title,
.hljs-section {
color: var(--base0D);
}
.hljs-keyword,
.hljs-selector-tag {
color: var(--base0E);
}
.hljs {
display: block;
overflow-x: auto;
color: var(--base05);
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-string, .hljs-link {
background: #FFA500;
user-select: text;
}

View file

@ -0,0 +1,916 @@
html {
-webkit-text-size-adjust: 100%;
box-sizing: border-box;
overflow-x: hidden !important;
}
*, *::before, *::after {
box-sizing: inherit;
}
body {
font-size: 15px;
overflow-x: hidden;
max-width: 100vw;
}
img,
video,
canvas,
iframe,
table {
max-width: 100% !important;
}
pre,
code {
white-space: pre-wrap !important;
word-break: break-word !important;
overflow-wrap: anywhere !important;
}
.header {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
padding: 4px !important;
gap: 4px !important;
overflow: visible !important;
}
.header-content {
display: flex !important;
flex-wrap: wrap !important;
width: 100% !important;
max-width: 100% !important;
padding: 2px !important;
gap: 4px !important;
overflow: visible !important;
}
.top-bar-left,
.top-bar-mid,
.top-bar-right {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
align-items: stretch !important;
gap: 6px !important;
}
.top-bar-left nav ul,
.top-bar-mid nav ul,
.top-bar-right nav ul {
display: flex !important;
flex-wrap: wrap !important;
gap: 6px !important;
padding: 0 !important;
margin: 0 !important;
overflow: visible !important;
}
.top-bar-left nav ul li,
.top-bar-mid nav ul li,
.top-bar-right nav ul li {
margin: 0 !important;
}
.top-bar-left nav ul li a,
.top-bar-mid nav ul li a,
.top-bar-right nav ul li a {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
padding: 6px 8px !important;
font-size: 0.9rem !important;
white-space: nowrap !important;
}
.search-input,
.feed-search-input,
.activity-search-input {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 40px !important;
font-size: 16px !important;
}
.main-content {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
gap: 12px !important;
}
.sidebar-left,
.sidebar-right,
.main-column {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
padding: 10px !important;
border-left: none !important;
border-right: none !important;
}
.sidebar-left {
order: 1 !important;
}
.main-column {
order: 3 !important;
}
.sidebar-right {
order: 2 !important;
}
.sidebar-left nav ul,
.sidebar-right nav ul {
display: flex !important;
flex-direction: column !important;
}
.oasis-nav-header {
font-size: 0.85rem !important;
}
.oasis-nav-list li a {
font-size: 0.9rem !important;
padding: 8px 12px !important;
}
button,
input[type="submit"],
input[type="button"],
.filter-btn,
.create-button,
.edit-btn,
.delete-btn,
.join-btn,
.leave-btn,
.buy-btn {
min-height: 44px !important;
font-size: 16px !important;
white-space: normal !important;
text-align: center !important;
}
.feed-row,
.comment-body-row,
table {
display: block !important;
width: 100% !important;
overflow-x: auto !important;
}
textarea,
input:not([type="checkbox"]):not([type="radio"]),
select {
width: 100% !important;
max-width: 100% !important;
font-size: 16px !important;
}
input[type="checkbox"]:not(.oasis-nav-toggle):not(.oasis-nav-side-toggle),
input[type="radio"] {
width: auto !important;
max-width: none !important;
flex: none !important;
}
.gallery {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: 8px !important;
}
footer,
.footer {
display: block !important;
width: 100% !important;
max-width: 100% !important;
padding: 12px !important;
overflow-x: auto !important;
}
footer div {
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
}
h1 { font-size: 1.35em !important; }
h2 { font-size: 1.2em !important; }
h3 { font-size: 1em !important; }
.small,
.time {
font-size: 0.8rem !important;
}
.created-at {
display: block !important;
width: 100% !important;
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: anywhere !important;
line-height: 1.5 !important;
font-size: 0.8rem !important;
}
.header-content .created-at {
display: block !important;
flex: 1 1 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
.post-meta,
.feed-post-meta,
.feed-row .small,
.feed-row .time,
.feed-row .created-at {
display: block !important;
width: 100% !important;
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: anywhere !important;
line-height: 1.4 !important;
}
.post-meta,
.feed-post-meta {
flex-direction: column !important;
gap: 4px !important;
}
.mode-buttons,
.mode-buttons-cols,
.mode-buttons-row,
.filter-group,
.activity-filter-grid,
.activity-sub-filter,
.filters .ui-toolbar,
.filter-group,
.inhabitant-action,
.mode-buttons {
display: flex !important;
flex-direction: row !important;
flex-wrap: wrap !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
max-width: 100% !important;
gap: 6px !important;
overflow-x: hidden !important;
scroll-snap-type: none !important;
grid-template-columns: none !important;
}
.mode-buttons .column,
.mode-buttons > div,
.activity-filter-col {
display: contents !important;
}
.mode-buttons form,
.filter-group form,
.inhabitant-action form,
.activity-filter-col form {
width: auto !important;
flex: 0 1 auto !important;
scroll-snap-align: none !important;
margin: 0 !important;
}
.inhabitant-action .filter-btn {
min-height: 0 !important;
padding: 7px 12px !important;
font-size: 13px !important;
white-space: nowrap !important;
}
.profile-sensors-box {
display: flex !important;
flex-direction: column !important;
align-items: flex-start !important;
gap: 6px !important;
width: 100% !important;
max-width: 100% !important;
}
.profile-sensors-box > * {
max-width: 100% !important;
word-break: break-word !important;
overflow-wrap: anywhere !important;
}
.inhabitant-activity-group {
display: flex !important;
flex-wrap: wrap !important;
gap: 6px !important;
}
.activity-filter-grid .filter-btn,
.activity-filter-col .filter-btn,
.activity-sub-filter .filter-btn {
min-height: 0 !important;
padding: 7px 12px !important;
font-size: 13px !important;
white-space: nowrap !important;
}
.mode-buttons .filter-btn,
.mode-buttons button,
.filter-group .filter-btn,
.filter-group button,
.activity-filter-grid .filter-btn,
.activity-filter-grid button,
.activity-sub-filter .filter-btn,
.filters .filter-btn,
.filters button {
width: auto !important;
flex: 0 0 auto !important;
white-space: nowrap !important;
min-width: 0 !important;
}
.inhabitant-card {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
overflow: visible !important;
}
.inhabitant-left {
width: 100% !important;
text-align: center !important;
flex: 0 0 auto !important;
height: auto !important;
}
.inhabitant-details {
width: 100% !important;
}
.inhabitant-left > a:first-of-type,
.inhabitant-left > a:first-child {
display: inline-block !important;
width: 150px !important;
height: 150px !important;
max-width: 150px !important;
margin: 0 auto 12px !important;
}
.inhabitant-photo,
.inhabitant-photo-details {
width: 150px !important;
height: 150px !important;
max-width: 150px !important;
object-fit: cover !important;
border-radius: 50% !important;
display: block !important;
margin: 0 auto !important;
}
.inhabitants-list,
.inhabitants-grid {
display: flex !important;
flex-direction: column !important;
gap: 10px !important;
}
.tribe-card {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
overflow: hidden !important;
}
.tribe-grid {
display: flex !important;
flex-direction: column !important;
gap: 10px !important;
grid-template-columns: 1fr !important;
}
.tribes-list,
.tribes-grid {
display: flex !important;
flex-direction: column !important;
gap: 10px !important;
}
.tribe-card-image {
width: 100% !important;
max-width: 100% !important;
}
.tribe-section-nav {
display: flex !important;
flex-wrap: wrap !important;
justify-content: center !important;
align-items: center !important;
gap: 6px !important;
overflow-x: visible !important;
width: 100% !important;
max-width: 100% !important;
}
.tribe-section-group {
display: contents !important;
}
.tribe-section-btn {
font-size: 12px !important;
padding: 7px 12px !important;
white-space: nowrap !important;
flex: 0 0 auto !important;
}
.tribe-details {
flex-direction: column !important;
}
.tribe-side {
width: 100% !important;
}
.tribe-main {
width: 100% !important;
}
.tribe-content-grid {
grid-template-columns: 1fr !important;
}
.tribe-inhabitants-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.tribe-media-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.tribe-overview-grid {
grid-template-columns: 1fr !important;
}
.tribe-mode-buttons {
flex-wrap: wrap !important;
}
.tribe-card-hero-image {
height: 180px !important;
}
.tribe-votation-option {
flex-direction: column !important;
align-items: flex-start !important;
}
.tribe-content-header {
flex-direction: column !important;
gap: 8px !important;
}
.tribe-content-filters {
flex-wrap: wrap !important;
}
.tribe-media-filters {
flex-wrap: wrap !important;
}
.forum-card {
flex-direction: column !important;
}
.forum-score-col,
.forum-main-col,
.root-vote-col {
width: 100% !important;
}
.forum-header-row {
flex-direction: column !important;
gap: 4px !important;
}
.forum-meta {
flex-wrap: wrap !important;
gap: 8px !important;
}
.forum-thread-header {
flex-direction: column !important;
}
.forum-comment {
margin-left: 0 !important;
padding-left: 8px !important;
}
.comment-body-row {
flex-direction: column !important;
}
.comment-vote-col,
.comment-text-col {
width: 100% !important;
}
.forum-score-box,
.forum-score-form {
flex-direction: row !important;
justify-content: center !important;
}
.new-message-form textarea,
.comment-textarea {
width: 100% !important;
}
[style*="grid-template-columns: repeat(6"] {
grid-template-columns: 1fr !important;
}
[style*="grid-template-columns: repeat(3"] {
grid-template-columns: 1fr !important;
}
[style*="grid-template-columns:repeat(6"] {
grid-template-columns: 1fr !important;
}
[style*="grid-template-columns:repeat(3"] {
grid-template-columns: 1fr !important;
}
[style*="grid-template-columns: repeat(auto-fit"] {
grid-template-columns: 1fr !important;
}
[style*="grid-template-columns:repeat(auto-fit"] {
grid-template-columns: 1fr !important;
}
[style*="width:50%"] {
width: 100% !important;
}
.inhabitant-karma-ubi .ubi-line{display:block;font-size:0.9em}
.pad-editor-container{flex-direction:column !important}
.pad-editor-area,.pad-members-list{width:100% !important;min-width:unset !important}
.chat-main-split{flex-direction:column !important}
.chat-messages-column,.chat-participants-column{width:100% !important;min-width:unset !important;flex:unset !important}
.games-grid{grid-template-columns:1fr !important}
.game-card{width:100% !important}
.main-column [class*="-row"],
.main-column [class*="-card"],
.main-column [class*="-item"],
.main-column [class*="-entry"],
.main-column .card,
.main-column article {
flex-wrap: wrap !important;
min-width: 0 !important;
}
.main-column [class*="-row"] > *,
.main-column [class*="-card"] > * {
min-width: 0 !important;
}
button:not([class]),
input[type="submit"]:not([class]),
input[type="button"]:not([class]),
.filter-btn,
a.filter-btn,
.conn-actions button,
.peers-conn-actions button {
background-color: #FFA500 !important;
color: #1a1a1a !important;
border: 1px solid #FFA500 !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
min-height: 42px !important;
padding: 9px 14px !important;
text-align: center !important;
white-space: normal !important;
max-width: 100% !important;
overflow-wrap: anywhere !important;
}
.filter-btn.active {
background-color: #FFD700 !important;
border-color: #FFD700 !important;
}
.audios-search,
.videos-search,
.images-search {
width: 100% !important;
max-width: 100% !important;
margin: 8px 0 !important;
}
.filter-box {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
gap: 8px !important;
align-items: stretch !important;
}
.filter-box__input {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 42px !important;
font-size: 16px !important;
box-sizing: border-box !important;
}
.filter-box__controls {
display: flex !important;
flex-direction: row !important;
flex-wrap: wrap !important;
width: 100% !important;
gap: 8px !important;
align-items: stretch !important;
}
.filter-box__controls .transfer-range {
display: flex !important;
flex-direction: row !important;
flex: 1 1 100% !important;
width: 100% !important;
gap: 8px !important;
}
.filter-box__number {
flex: 1 1 0 !important;
min-width: 0 !important;
width: auto !important;
height: 42px !important;
font-size: 15px !important;
box-sizing: border-box !important;
}
.filter-box__select {
flex: 1 1 auto !important;
min-width: 0 !important;
height: 42px !important;
font-size: 15px !important;
}
.filter-box__button {
flex: 0 0 auto !important;
width: auto !important;
min-height: 42px !important;
white-space: nowrap !important;
background-color: #FFA500 !important;
color: #1a1a1a !important;
border: 1px solid #FFA500 !important;
border-radius: 8px !important;
font-weight: 600 !important;
padding: 9px 16px !important;
}
.filters .ui-toolbar--filters {
justify-content: center !important;
align-items: center !important;
gap: 6px !important;
}
.main-column button,
.main-column input,
.main-column select,
.main-column textarea,
.main-column .btn,
.main-column .create-button,
.main-column .update-btn,
.main-column .delete-btn,
.main-column .edit-btn,
.main-column .join-btn,
.main-column .leave-btn,
.main-column .buy-btn,
.main-column .submit-button {
max-width: 100% !important;
box-sizing: border-box !important;
overflow-wrap: anywhere !important;
}
.main-column button,
.main-column .btn,
.main-column .create-button,
.main-column .update-btn,
.main-column .delete-btn,
.main-column .submit-button {
white-space: normal !important;
}
.main-column form {
max-width: 100% !important;
min-width: 0 !important;
}
.card-field {
display: flex !important;
flex-wrap: wrap !important;
min-width: 0 !important;
max-width: 100% !important;
gap: 4px !important;
}
.card-field .card-value,
.card-field .card-label {
min-width: 0 !important;
max-width: 100% !important;
white-space: normal !important;
overflow-wrap: anywhere !important;
word-break: break-word !important;
}
.card-value .user-link,
.card-value a {
white-space: normal !important;
max-width: 100% !important;
overflow-wrap: anywhere !important;
}
.market-grid,
.shop-products-grid,
.jobs-grid {
display: grid !important;
grid-template-columns: 1fr !important;
gap: 12px !important;
width: 100% !important;
max-width: 100% !important;
}
.market-card,
.market-item {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
.cycle-info {
display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 8px !important;
}
.kpi {
min-width: 0 !important;
padding: 8px !important;
}
.kpi__label {
letter-spacing: 0 !important;
word-break: normal !important;
overflow-wrap: break-word !important;
font-size: 11px !important;
}
.kpi__value {
word-break: break-word !important;
overflow-wrap: anywhere !important;
font-size: 13px !important;
}
.table-wrap {
display: block !important;
width: 100% !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
.table-wrap table,
.table-wrap .table {
display: table !important;
width: 100% !important;
max-width: 100% !important;
table-layout: fixed !important;
}
.table-wrap th,
.table-wrap td {
white-space: normal !important;
word-break: normal !important;
overflow-wrap: break-word !important;
font-size: 10px !important;
padding: 4px 3px !important;
text-align: center !important;
}
.search-filters-row {
display: block !important;
width: 100% !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
.search-filters-table,
.search-filters-table tbody,
.search-filters-table tr,
.search-filters-table td {
display: block !important;
width: 100% !important;
max-width: 100% !important;
overflow-x: hidden !important;
}
.search-filters-table td {
padding: 4px 0 !important;
}
.search-filters-table input,
.search-filters-table select,
.search-oasis-id {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
box-sizing: border-box !important;
margin-left: 0 !important;
}
.oasis-id-copy {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
font-family: monospace !important;
font-size: 12px !important;
text-align: center !important;
padding: 8px !important;
border: 1px solid #FFA500 !important;
border-radius: 8px !important;
background: rgba(255, 165, 0, 0.08) !important;
color: inherit !important;
-webkit-user-select: all !important;
user-select: all !important;
}
.peer-key,
.user-link.peer-key {
-webkit-user-select: all !important;
user-select: all !important;
word-break: break-all !important;
}
.create-button,
button.create-button,
a.create-button {
background-color: #ffcc00 !important;
border-color: #e6b800 !important;
color: #000 !important;
}
.peers-list .block-info-table,
.peers-technical-block .block-info-table,
.gov-overview {
display: block !important;
width: 100% !important;
max-width: 100% !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
.peers-list .block-info-table tbody,
.peers-technical-block .block-info-table tbody,
.gov-overview tbody {
display: block !important;
width: 100% !important;
}
.gov-overview thead { display: none !important; }
.peers-list .block-info-table tr:first-child,
.peers-technical-block .block-info-table tr:first-child {
display: none !important;
}
.peers-list .block-info-table tr,
.peers-technical-block .block-info-table tr,
.gov-overview tbody tr {
display: block !important;
width: 100% !important;
box-sizing: border-box !important;
border: 1px solid rgba(255, 165, 0, 0.35) !important;
border-radius: 8px !important;
margin: 0 0 8px 0 !important;
padding: 6px 10px !important;
background: rgba(255, 165, 0, 0.04) !important;
}
.peers-list .block-info-table td,
.peers-technical-block .block-info-table td,
.gov-overview tbody td {
display: flex !important;
justify-content: space-between !important;
align-items: baseline !important;
gap: 10px !important;
width: 100% !important;
border: 0 !important;
padding: 3px 0 !important;
font-size: 12px !important;
text-align: right !important;
word-break: break-all !important;
overflow-wrap: anywhere !important;
}
.peers-list .block-info-table td::before,
.peers-technical-block .block-info-table td::before,
.gov-overview tbody td::before {
content: attr(data-label);
flex: 0 0 auto !important;
font-weight: 700 !important;
text-transform: uppercase;
font-size: 10px !important;
letter-spacing: .03em;
opacity: .7 !important;
text-align: left !important;
white-space: nowrap !important;
word-break: normal !important;
}
.peers-technical-block .block-info-table td[data-label=""] { justify-content: center !important; }
.peers-technical-block .block-info-table td[data-label=""]::before { content: none !important; }
.peers-list .block-info-table td .peer-key,
.peers-technical-block .block-info-table td .peer-key { text-align: right !important; }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
const path = require('path');
let i18n = {};
const languages = ['en', 'es', 'fr', 'eu', 'de', 'it', 'pt', 'zh', 'ar', 'hi', 'ru'];
languages.forEach(language => {
try {
const languagePath = path.join(__dirname, `oasis_${language}.js`);
const languageData = require(languagePath);
i18n[language] = languageData[language];
} catch (error) {
console.error(`Failed to load language file for ${language}:`, error);
}
});
module.exports = i18n;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
module.exports = {
feed: 'createFeedStream',
history: 'createHistoryStream',
hist: 'createHistoryStream',
public: 'getPublicKey',
pub: 'getPublicKey',
log: 'createLogStream',
logt: 'messagesByType',
conf: 'config',
};

124
src/client/gui.js Normal file
View file

@ -0,0 +1,124 @@
const path = require('path');
const fs = require('fs');
const os = require('os');
const debug = require('../server/node_modules/debug')('oasis');
const lodash = require('../server/node_modules/lodash');
const ssbClient = require('../server/node_modules/ssb-client');
const ssbConfig = require('../server/node_modules/ssb-config');
const ssbKeys = require('../server/node_modules/ssb-keys');
const { printMetadata } = require('../server/ssb_metadata');
const updateFlagPath = path.join(__dirname, "../server/.update_required");
let internalSSB = null;
try {
const { server } = require('../server/SSB_server');
internalSSB = server;
} catch {}
if (process.env.OASIS_TEST) {
ssbConfig.path = fs.mkdtempSync(path.join(os.tmpdir(), "oasis-"));
ssbConfig.keys = ssbKeys.generate();
}
const socketPath = path.join(ssbConfig.path, "socket");
const publicInteger = ssbConfig.keys.public.replace(".ed25519", "");
const remote = `unix:${socketPath}~noauth:${publicInteger}`;
const connect = (options) =>
new Promise((resolve, reject) => {
ssbClient(process.env.OASIS_TEST ? ssbConfig.keys : null, options)
.then(resolve)
.catch(reject);
});
let closing = false;
let clientHandle;
const attemptConnectionWithBackoff = (attempt = 1) => {
const maxAttempts = 5;
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
return new Promise((resolve, reject) => {
connect({ remote })
.then(resolve)
.catch((error) => {
if (attempt >= maxAttempts) {
return reject(new Error("Failed to connect after multiple attempts"));
}
setTimeout(() => {
attemptConnectionWithBackoff(attempt + 1).then(resolve).catch(reject);
}, delay);
});
});
};
let pendingConnection = null;
const ensureConnection = (customConfig) => {
if (pendingConnection === null) {
pendingConnection = new Promise((resolve) => {
setTimeout(() => {
attemptConnectionWithBackoff()
.then(resolve)
.catch(() => {
resolve(null);
});
});
});
const cancel = () => (pendingConnection = null);
pendingConnection.then(cancel, cancel);
}
return pendingConnection;
};
module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false }) => {
const customConfig = JSON.parse(JSON.stringify(ssbConfig));
if (offline === true) {
lodash.set(customConfig, "conn.autostart", false);
}
lodash.set(
customConfig,
"conn.hops",
lodash.get(ssbConfig, "conn.hops", lodash.get(ssbConfig.friends, "hops", 0))
);
const cooler = {
open() {
return new Promise((resolve, reject) => {
if (internalSSB) {
const { printMetadata, colors } = require('../server/ssb_metadata');
printMetadata('OASIS GUI', colors.yellow, port, host, offline, isPublic);
return resolve(internalSSB);
}
if (clientHandle && clientHandle.closed === false) {
return resolve(clientHandle);
}
ensureConnection(customConfig).then((ssb) => {
if (!ssb) return reject(new Error("No SSB server available"));
clientHandle = ssb;
if (closing) {
cooler.close();
reject(new Error("Closing Oasis"));
} else {
const { printMetadata, colors } = require('../server/ssb_metadata');
printMetadata('OASIS GUI', colors.yellow, port, host, offline, isPublic);
resolve(ssb);
}
}).catch(reject);
});
},
close() {
closing = true;
if (clientHandle && clientHandle.closed === false) {
clientHandle.close();
}
},
};
return cooler;
};

210
src/client/middleware.js Normal file
View file

@ -0,0 +1,210 @@
const path = require("path");
const os = require("os");
const Koa = require(path.join(__dirname, "../server/node_modules/koa"));
const koaStatic = require(path.join(__dirname, "../server/node_modules/koa-static"));
const { join } = require("path");
const mount = require(path.join(__dirname, "../server/node_modules/koa-mount"));
function obfuscateClearnetHtml(html) {
if (typeof html !== 'string' || html.length === 0) return html;
const preserve = [];
const stash = (re) => {
html = html.replace(re, (m) => {
preserve.push(m);
return `${preserve.length - 1}`;
});
};
stash(/<pre[\s\S]*?<\/pre>/gi);
stash(/<textarea[\s\S]*?<\/textarea>/gi);
stash(/<style[\s\S]*?<\/style>/gi);
html = html.replace(/<!--[\s\S]*?-->/g, '');
html = html.replace(/>[\s\n\r\t]+</g, '><');
html = html.replace(/[ \t]{2,}/g, ' ');
html = html.replace(/[\r\n]+/g, '');
html = html.replace(/(\d+)/g, (_, i) => preserve[Number(i)] || '');
return html;
}
const collectLocalIPs = () => {
const out = [];
try {
const ifaces = os.networkInterfaces();
for (const name of Object.keys(ifaces)) {
for (const info of (ifaces[name] || [])) {
if (info && !info.internal && (info.family === 'IPv4' || info.family === 4)) {
out.push(info.address);
}
}
}
} catch (_) {}
return out;
};
module.exports = ({ host, port, middleware, allowHost }) => {
const assets = new Koa()
assets.use(koaStatic(join(__dirname, "..", "client", "assets")));
const app = new Koa();
const validHosts = [];
const isClearnetPath = (request) => {
const url = String(request.url || '');
return url === '/c' || url.startsWith('/c/') || url.startsWith('/c?');
};
const isValidRequest = (request) => {
if (isClearnetPath(request)) return request.method === 'GET';
if (validHosts.includes(request.hostname) !== true) {
return false;
}
if (request.method !== "GET") {
if (request.header.referer == null) {
return false;
}
try {
const refererUrl = new URL(request.header.referer);
if (validHosts.includes(refererUrl.hostname) !== true) {
return false;
}
if (refererUrl.pathname.startsWith("/blob/")) {
return false;
}
} catch (e) {
return false;
}
}
return true;
};
app.on("error", (err, ctx) => {
if (err && (err.code === 'ECONNRESET' || err.code === 'EPIPE')) {
return;
}
if (err && (err.name === 'BadRequestError' || err.status === 400)) {
console.error(`[400] ${err.message}`);
return null;
}
console.error(err);
if (ctx && isValidRequest(ctx.request)) {
err.message = err.message || 'Internal server error';
err.expose = true;
}
return null;
});
app.use(mount("/assets", assets));
const maptiles = new Koa();
maptiles.use(koaStatic(join(__dirname, "..", "maps", "tiles")));
app.use(mount("/maptiles", maptiles));
const mapcache = new Koa();
mapcache.use(koaStatic(join(__dirname, "..", "maps", "cache")));
app.use(mount("/mapcache", mapcache));
const gamesStatic = new Koa();
gamesStatic.use(koaStatic(join(__dirname, "..", "games")));
app.use(mount("/game-assets", gamesStatic));
app.use(mount("/js", koaStatic(path.join(__dirname, 'public/js'))));
app.use(koaStatic(path.join(__dirname, 'public')));
app.use(async (ctx, next) => {
//console.log("Requesting:", ctx.path); // uncomment to check for HTTP requests
const isClearnet = isClearnetPath(ctx.request);
const csp = isClearnet
? [
"default-src 'self'",
"script-src 'none'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"media-src 'self' blob:",
"connect-src 'self'",
"form-action 'self'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'"
].join("; ")
: [
"default-src 'self'",
"script-src 'self' http://localhost:3000/js",
"style-src 'self'",
"img-src 'self'",
"media-src 'self' blob:",
"worker-src 'self' blob:",
"frame-src 'self'",
"form-action 'self'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'"
].join("; ");
ctx.set("Content-Security-Policy", csp);
ctx.set("X-Frame-Options", "SAMEORIGIN");
ctx.set("X-Content-Type-Options", "nosniff");
ctx.set("Referrer-Policy", "same-origin");
ctx.set("Permissions-Policy", "speaker=(self)");
const validHostsString = validHosts.join(" or ");
ctx.assert(
isValidRequest(ctx.request),
400,
`Request must be addressed to ${validHostsString} and non-GET requests must contain non-blob referer.`
);
await next();
if (isClearnet && typeof ctx.body === 'string') {
const type = String(ctx.response.type || ctx.response.get('Content-Type') || '').toLowerCase();
if (type.includes('html')) {
ctx.body = obfuscateClearnetHtml(ctx.body);
}
}
});
// pdf viewer
const pdfjsPath = path.join(__dirname, '../server/node_modules/pdfjs-dist/build/pdf.min.js');
app.use(koaStatic(pdfjsPath));
middleware.forEach((m) => app.use(m));
const server = app.listen({ host, port });
server.on("listening", () => {
const address = server.address();
if (typeof address === "string") {
throw new Error("HTTP server should never bind to Unix socket");
}
if (allowHost !== null) {
validHosts.push(allowHost);
}
validHosts.push(address.address);
if (validHosts.includes(host) === false) {
validHosts.push(host);
}
for (const ip of collectLocalIPs()) {
if (validHosts.includes(ip) === false) validHosts.push(ip);
}
for (const loopback of ['localhost', '127.0.0.1']) {
if (validHosts.includes(loopback) === false) validHosts.push(loopback);
}
});
return server;
};

View file

@ -0,0 +1,70 @@
"use strict";
const path = require('path');
const yargs = require(path.join(__dirname, '../server/node_modules/yargs'));
const { hideBin } = require(path.join(__dirname, '../server/node_modules/yargs/helpers'));
const _ = require(path.join(__dirname, '../server/node_modules/lodash'));
const moduleAlias = require(path.join(__dirname, '../server/node_modules/module-alias'));
moduleAlias.addAlias('punycode', 'punycode/');
const HELP_TEXT = `Usage: sh oasis.sh [mode] [options]
Modes:
gui Launch the Oasis web GUI (default if no mode given).
server, pub Launch only the Oasis Sbot (headless, PUB mode).
help, -h, --help Show this help message.
PUB admin commands (require the Oasis Sbot to be running):
whoami Print this Oasis ID.
invite [N] Create an invite code. N = number of uses (default 1).
name <text> Set this Oasis display name.
announce <host> [port] Publish a pub address (default port 8008).
follow <feedId> Follow another Oasis ID / feed.
status Show peer / replication status.
gossip List known gossip peers.
GUI options (forwarded to the backend):
--host=<ip> Hostname / IP the web UI listens on (default: localhost).
Use 0.0.0.0 to expose on a VPS.
--port=<n> Port for the web UI (default: 3000).
--allow-host=<host> Extra hostname allowed when behind a reverse proxy.
--public Public-hosting mode: disables POST and redacts content
from people who haven't opted in to public hosting.
--offline Don't connect to Oasis peers or pubs.
--no-open Don't auto-open a browser tab on launch (useful on VPS).
--debug Verbose logging.
Examples:
sh oasis.sh
sh oasis.sh server
sh oasis.sh invite 100
sh oasis.sh name "My PUB"
sh oasis.sh announce mypub.example.com
sh oasis.sh --host=0.0.0.0 --port=8080 --no-open
`;
const cli = (presets /*, defaultConfigFile */) => {
const argv = process.argv.slice(2);
if (argv.includes('-h') || argv.includes('--help') || argv.includes('-help') || argv.includes('help')) {
process.stdout.write(HELP_TEXT);
process.exit(0);
}
return yargs(hideBin(process.argv))
.scriptName("oasis")
.env("OASIS")
.help(false)
.version(false)
.options("open", { default: _.get(presets, "open", true), type: "boolean" })
.options("offline", { default: _.get(presets, "offline", false), type: "boolean" })
.options("host", { default: _.get(presets, "host", "localhost"), type: "string" })
.options("allow-host", { default: _.get(presets, "allow-host", null), type: "string" })
.options("port", { default: _.get(presets, "port", 3000), type: "number" })
.options("public", { default: _.get(presets, "public", false), type: "boolean" })
.options("debug", { default: _.get(presets, "debug", false), type: "boolean" })
.parserConfiguration({ "strip-aliased": true })
.argv;
};
module.exports = { cli };

View file

@ -0,0 +1,115 @@
ECOin - Copyright (c) - 2014/2025 - GPLv3 - epsylon@riseup.net (https://ecoin.03c8.net)
===========================================
# SOURCES for P2P Crypto-Currency (ECOin) #
===========================================
Testing machine is: Debian GNU/Linux 12 (bookworm) (x86_64).
All of the commands should be executed in a shell.
------------------------------
(0.) Clone the github tree to get the source code:
+ Official:
git clone http://code.03c8.net/epsylon/ecoin
+ Mirror:
git clone https://github.com/epsylon/ecoin
------------------------------
===================================================
# SERVER -ecoind- for P2P Crypto-Currency (ECOin) #
===================================================
(0.) Version libraries:
- Libboost -> source code 1.68 provided at: src/boost_1_68_0
(1.) Install dependencies:
sudo apt-get install build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev libleveldb-dev miniupnpc libminiupnpc-dev
+ Optionally install qrencode (and set USE_QRCODE=1):
sudo apt-get install libqrencode-dev
(2.) Now you should be able to build ecoind:
cd src/
make -f makefile.linux USE_UPNP=- USE_IPV6=-
strip ecoind
An executable named 'ecoind' will be built.
Now you can launch: ./ecoind to run your ECOin server.
------------------------------
============================================
# WALLET for P2P Crypto-Currency (ECOin) #
============================================
(0.) Version libraries:
- Libboost -> source code 1.68 provided at: src/boost_1_68_0
(1.) First, make sure that the required packages for Qt5 development (an the others required for building the daemon) are installed:
sudo apt-get install qt5-qmake qtbase5-dev build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev libleveldb-dev miniupnpc libminiupnpc-dev
+ Optionally install qrencode (and set USE_QRCODE=1):
sudo apt-get install libqrencode-dev
(2.) Then execute the following:
qmake USE_UPNP=- USE_IPV6=-
make
An executable named 'ecoin-qt' will be built.
Now you can launch: ./ecoin-qt to run your ECOin wallet/GUI.
------------------------------
========================================
+ CPU MINER for ECOin (Unix/GNU-Linux) #
========================================
See doc/MINING.txt for detailed instructions on running /ecoin-miner/ on different platforms.
+ GNU/Linux:
cd miner/
sh build.sh
An executable named 'cpuminer' will be built.
Now you can launch: ./cpuminer to run your ECOin PoW miner.
======================================
For a list of command-line options:
./ecoind --help
To start the ECOin daemon:
./ecoind -daemon
For ECOin-QT Wallet
./ecoin-qt
For debugging:
tail -f /home/$USER/.ecoin/debug.log
======================================